1.synchronized关键字
synchronized关键字提供了一种排他机制,也就是同一时间只能有一个线程执行某些操作,从而防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的索引读或者写都将通过同步的方式来进行,表现如下:
1)synchronized关键字提供一种锁机制,能够确保变量的互斥访问,从而防止数据不一致问题出现。
2)synchronized关键字包含monitor enter 和 monitor exit两个jvm指令,它能够保证任何线程执行到monitor enter成功之前必须从主内存获取数据,而不是从缓存中,在执行monitor exit成功之后,共享变量被更新后的值必须刷入主内存。
3)synchronized的指令严格遵循java happens-before规则,一个monitor exit指令前必定有一个monitor enter
1.1synchronized关键字用法
1)同步方法
[default|public|private|protected] synchronized [static] type method(){}
2)同步代码块
private final Object MUTEX = new Object(); public void sync(){ synchronized(MUTEX){ ... ... } }
注意:synchronized关键字不可以用到变量上,对公共变量的同步可以使用上面两种同步方法,示例代码:
public class TicketWindowRunable1 implements Runnable{ private int index = 1; private int max = 100; private static final Object MUTEX = new Object(); @Override public void run() { synchronized (MUTEX) { //注意,一定要将对公共数据的所有操作放到同步代码中,包括下面的while判断 while(index <= max) { try { TimeUnit.MILLISECONDS.sleep(1);//模拟数据处理耗时 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("请编号为"+index ++ + "的客户到"+Thread.currentThread().getName()+"办理业务!"); } } } public static void main(String[] args) { TicketWindowRunable1 tr = new TicketWindowRunable1(); Thread thread1 = new Thread(tr, "1号窗口"); Thread thread2 = new Thread(tr, "2号窗口"); Thread thread3 = new Thread(tr, "3号窗口"); thread1.start(); thread2.start(); thread3.start(); } }
1.2synchronized关键字注意问题
1)monitor关联的对象不能为空
2)synchronized作用域不宜太大
由于synchronized的排他性,synchronized作用域太长会影响效率
3)不同monitor企图锁相同的代码
如下代码:
public class ErrorMonitor implements Runnable{ private final Object MUTEX = new Object(); public void run() { synchronized (MUTEX) { System.out.println(MUTEX); } } public static void main(String[] args) throws InterruptedException { for(int i = 0;i < 5;i ++) { new Thread(new ErrorMonitor()).start(); } TimeUnit.SECONDS.sleep(10); } }
输出结果:
java.lang.Object@f5cbdd
java.lang.Object@4b20c054
java.lang.Object@7ff41d1f
java.lang.Object@3dcbd823
java.lang.Object@163bd47a
上面代码中,MUTEX不是静态的,每创建一个ErrorMonitor对象都会创建一个MUTEX,main方法中5个线程各自创建了一个,因此起不到锁的作用,其特点是MUTEX属于对象,不同线程使用的是不同的对象。
如果改成下面写法,则就没有问题:
public class ErrorMonitor implements Runnable{ private final Object MUTEX = new Object(); public void run() { synchronized (MUTEX) { System.out.println(MUTEX); } } public static void main(String[] args) throws InterruptedException { ErrorMonitor target = new ErrorMonitor(); for(int i = 0;i < 5;i ++) { new Thread(target).start(); } TimeUnit.SECONDS.sleep(10); } }
输出结果:
java.lang.Object@5405ed04
java.lang.Object@5405ed04
java.lang.Object@5405ed04
java.lang.Object@5405ed04
java.lang.Object@5405ed04
4)多个锁交叉导致死锁
private final Object MUTEX_read = new Object(); private final Object MUTEX_write = new Object(); public void read() { synchronized (MUTEX_read) { synchronized (MUTEX_write) { //do } } } public void write() { synchronized (MUTEX_write) { synchronized (MUTEX_read) { //do } } }
1.3 this monitor 与 class monitor
synchronized(this)的monitor对象是当前类的实例对象,synchronized修饰的成员方法也是当前实例对象。
静态方法的monitor对象是类的Class对象
1.4死锁
死锁可能的原因:
1)交叉锁导致死锁:线程A持有R1的锁等待获取R2的锁,线程B持有R2的锁等待R1的锁
2)内存不足:当内存不足时,两个线程都在等待对方释放锁
3)一问一答的数据交换:
服务端开启某个端口等待客户端访问,客户端发起请求立即等待接收,由于某种原因服务端错过客户端的请求导致两端都陷入等待
4)数据库锁
某个线程执行for update语句意为退出事务,其他线程访问被锁资源时都将陷入死锁
5)文件锁
某个线程获得了文件锁意外退出,其他读取该文件的线程会进入死锁直到系统释放文件句柄资源
6)死循环引起死锁
由于代码原因或某些处理不得当,进入死循环,虽然查看线程栈信息不回发现任何死锁迹象,但是程序不工作,CPU占有率居高不下,这种死锁一般称之为系统假死,为一种最为致命也最难排查的死锁现象
死锁举例:
HashMap不是线程安全的类,如果多个线程对其进行写操作,很有可能出现死循环引起的死锁。程序运行一段时间后CPU等资源居高不下,各种诊断工具很难派上用场,因为死锁引起的进程往往会榨干CPU等几乎所有资源,诊断工具由于缺少资源一时难以启动。如下代码:
public class DeadBlockTest { private static final HashMap<String, String> map = new HashMap<>(); public void add(String key, String value) { map.put(key, value); } public static void main(String[] args) { final DeadBlockTest dt = new DeadBlockTest(); for (int m = 0; m < 2; m++) { new Thread(() -> { for (int x = 0; x < Integer.MAX_VALUE; x++) { //System.out.println(Thread.currentThread().getName() + ":" + x); dt.add(x + "", x + ""); } }, "T-" + m).start(); } } }
HashMap不具备线程安全的能力,如果想要使用线程安全的map结构请使用ConcurrentHashMap或者使用Collection.synchronizedMap