synchronized同步方法 方法内的变量为线程安全 “非线程安全”问题存在与实例变量中,如果是方法内部的私有变量,则不存在“非线程安全”问题,也就是线程安全的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class MyThread extends Thread { @Override public void run () { int count = 5 ; System.out.println("需要执行的任务" ); while (count > 0 ) { count--; System.out.println("由:" + Thread.currentThread().getName() + "计算,count=" + count); } } public static void main (String[] args) { MyThread myThread = new MyThread(); Thread a = new Thread(myThread, "A" ); Thread b = new Thread(myThread, "B" ); Thread c = new Thread(myThread, "C" ); a.start(); b.start(); c.start(); System.out.println("运行结束!" ); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 需要执行的任务 需要执行的任务 由:B计算,count=4 由:B计算,count=3 由:B计算,count=2 由:B计算,count=1 由:B计算,count=0 由:A计算,count=4 由:A计算,count=3 由:A计算,count=2 由:A计算,count=1 由:A计算,count=0 运行结束! 需要执行的任务 由:C计算,count=4 由:C计算,count=3 由:C计算,count=2 由:C计算,count=1 由:C计算,count=0
尽管打印的内容不规则,但是线程安全的。
实例变量非线程安全 如果多个线程共同访问同一个对象中的实例变量,则可能出现“非线程安全问题”。
更改上面的程序,将方法内私有变量更改为,自定义线程的私有变量。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class MyThread extends Thread { private int count = 5 ; @Override public void run () { System.out.println("需要执行的任务" ); count--; System.out.println("由:" + Thread.currentThread().getName() + "计算,count=" + count); } public static void main (String[] args) { MyThread myThread = new MyThread(); Thread a = new Thread(myThread, "A" ); Thread b = new Thread(myThread, "B" ); Thread c = new Thread(myThread, "C" ); a.start(); b.start(); c.start(); System.out.println("运行结束!" ); } }
结果如下:
1 2 3 4 5 6 7 需要执行的任务 需要执行的任务 由:B计算,count=3 由:A计算,count=3 运行结束! 需要执行的任务 由:C计算,count=2
结果出现了非线程安全问题,假如这个是抢票,则会出现大问题了,解决此问题只需要在方法(run方法)前加上synchronized
关键字即可,更改后运行结果为:
1 2 3 4 5 6 7 需要执行的任务 由:A计算,count=4 需要执行的任务 由:B计算,count=3 运行结束! 需要执行的任务 由:C计算,count=2
没有再出数据错误的情况,这样转换为同步方法就解决的线程安全问题。
多个对象多个锁 synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,当多个线程同时访问同步方法时,哪个线程先执行同步方法,哪个线程就持有该方法所属对象的锁lock,其他线程只能等待持有锁的线程访问完毕,在进行争夺锁。
synchronized方法与锁对象 调用关键字synchronized
声明的方法一定排队运行的,另外需要牢记“共享”两个字,只有共享资源的读写访问才需要同步化,如果不是共享的资源,就没有使用同步的必要。
线程A和线程B,如果A现持有了对象的Lock
锁访问synchronized
方法,那么B无法访问被此同步方法,若一定要访问此同步方法则需等待,但是B可以异步调用非synchronized
类型的方法。
脏读 为了避免数据出现交叉、出错,使用synchronized
关键字来进行同步,虽然在赋值的时候进行同步,但在取值的时候有可能出现一些意想不到的意外,这种情况就是脏读,发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了。解决办法就是在取值的方法上加上synchronized
关键字使其成为同步方法,这样就必须等待其他一系列的操作完成后,在进行读取,这样就避免来脏读。总结:
当A线程调用anyObject对象加入synchronized
关键字的X方法时,A线程就获得了X方法锁,更准备地讲,是获得了对象的锁,所以其他线程必须等A线程执行完才可以调用X方法,但B线程可以随意调用其他的非synchronized
同步方法。
当A线程调用anyObject对象加入synchronized
关键字的X方法时,A线程就获得了X方法所在对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,而B线程如果调用声明了synchronized
关键字的非X方法时,必须等A线程将X方法执行完,也就是释放对象锁后才可以调用。这时A线程已经执行了一个完整的任务,也就是说一些变量的赋值已经完成不存在脏读的环境了。
synchronized锁重入 关键字synchronized
拥有锁重入的功能,也就是在使用synchronized
时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。一个synchronized
方法/块的内部调用本类的其他synchronized
方法/块时,是永远可以得到锁的。
出现异常锁自动释放 线程a持用对象的锁,此时发生异常则a会是释放锁,以便其他线程能够成功拿到锁来进行访问。
同步不具有继承性 同步不能继承,如果子类重写了父类的同步方法,则需要在子类的同步方法中添加synchronized
关键字,否则无法同步。
同步语句块 用关键字synchronized
声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行了一个长时间的任务,那么B线程则必须等待较长时间。在这样的情况下就要考虑使用synchronized
同步代码块来解决。
synchronized同步代码块的使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public class MyService { public void serviceMethod () { try { synchronized (this ) { System.out.println("begin time = " + new Date()); Thread.sleep(2000 ); System.out.println("end time = " + new Date()); } } catch (Exception e) { e.printStackTrace(); } } static class ThreadA extends Thread { private MyService myService; ThreadA(MyService myService) { this .myService = myService; } @Override public void run () { super .run(); myService.serviceMethod(); } } static class ThreadB extends Thread { private MyService myService; ThreadB(MyService myService) { this .myService = myService; } @Override public void run () { super .run(); myService.serviceMethod(); } } public static void main (String[] args) { MyService myService = new MyService(); MyService.ThreadA threadA = new MyService.ThreadA(myService); threadA.setName("a" ); threadA.start(); MyService.ThreadB threadB = new MyService.ThreadB(myService); threadB.setName("b" ); threadB.start(); } }
运行结果为:
1 2 3 4 begin time = Tue Feb 19 13:51:49 CST 2019 end time = Tue Feb 19 13:51:51 CST 2019 begin time = Tue Feb 19 13:51:51 CST 2019 end time = Tue Feb 19 13:51:53 CST 2019
结果来看并未出现非线程安全问题,但是执行效率确实很低并没有提高,执行的效果还是同步运行的。
用同步代码块解决同步方法的弊端(一半同步,一半异步) 修改上面MyService中serviceMethod()方法,其他不变:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void serviceMethod () { try { for (int i = 0 ; i < 5 ; i++) { System.out.println("nosynchronized threadName=" + Thread.currentThread().getName() + " i=" + (i + 1 )); Thread.sleep(1000 ); } synchronized (this ) { for (int i = 0 ; i < 5 ; i++) { System.out.println("synchronized threadName=" + Thread.currentThread().getName() + " i=" + (i + 1 )); Thread.sleep(1000 ); } } } catch (Exception e) { e.printStackTrace(); } }
运行结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 nosynchronized threadName=a i=1 nosynchronized threadName=b i=1 nosynchronized threadName=a i=2 nosynchronized threadName=b i=2 nosynchronized threadName=b i=3 nosynchronized threadName=a i=3 nosynchronized threadName=b i=4 nosynchronized threadName=a i=4 nosynchronized threadName=b i=5 nosynchronized threadName=a i=5 synchronized threadName=b i=1 synchronized threadName=b i=2 synchronized threadName=b i=3 synchronized threadName=b i=4 synchronized threadName=b i=5 synchronized threadName=a i=1 synchronized threadName=a i=2 synchronized threadName=a i=3 synchronized threadName=a i=4 synchronized threadName=a i=5
由结果可知,当一个线程访问object的一个synchronized
同步代码块时,另一个线程仍然可以访问该object对象中的非synchronized(this)
同步代码块,有效的提高了运行效率。
synchronized代码块间的同步性
在使用synchronized(this)
代码块时需要注意的时,当一个线程访问object的一个synchronized(this)
同步代码块时,其他线程对同一个object中所有其他synchronized(this)
同步代码块的访问将被阻塞,这说明synchronized
使用的“对象监视器”是同一个。
和synchronized
方法一样,synchronized(this)
代码块是锁定当前对象的。
将任意对象作为对象监视器
多个线程调用同一个对象中的不同名称的synchronized
同步方法或synchronized(this)
时,调用效果就是按顺序进行,也就是同步,阻塞的。
无论是synchronized(this)
代码块还是synchronized
同步方法,同一时间只有一个线程可以执行synchronized(this)
代码块中的代码或synchronized
同步方法,对其他synchronized(this)
代码块中的代码或synchronized
同步方法的调用都是呈阻塞状态的。
Java中支持对“任意对象”作为“对象监视器”来实现同步的功能。这个对象大多是实例变量及方法中的参数,使用格式synchronized(非this对象x)
在多个线程持有“对象监视器”作为同一个对象时,同一时间只有一个线程可以执行synchronized(非this对象x)
同步代码中的代码
当持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)
同步代码中的代码
synchronized(非this对象x)
优点:在一个类中有多个synchronized
方法,这时虽然能实现同步,但是会收到阻塞,所以影响效率;但如果使用synchronized(非this对象x)
同步代码块,则synchronized(非this对象x)
代码块中的代码和synchronized
同步方法是异步的,不会和其他this锁同步方法或同步代码块争抢this锁,可以大大提高运行效率。
synchronized(非this对象x)
同步代码块中非this对象x
不同时,则是异步效果不会阻塞。
同步代码块放在非同步synchronized
方法中声明,并不能保证调用方法的线程的执行同步/顺序性,也就是线程调用方法的顺序是无序的,虽然同步块中执行的顺序是同步的,这样极容易出现脏读,使用synchronized(非this对象x)
同步代码块的格式可以解决这个脏读问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public class MyService { public void serviceMethod (MyList myList, String data) { try { if (myList.getSize() < 1 ) { Thread.sleep(2000 ); myList.add(data); } } catch (InterruptedException e) { e.printStackTrace(); } } static class ThreadA extends Thread { private MyList myList; ThreadA(MyList myList) { this .myList = myList; } @Override public void run () { super .run(); new MyService().serviceMethod(myList, "threadA" ); } } static class ThreadB extends Thread { private MyList myList; ThreadB(MyList myList) { this .myList = myList; } @Override public void run () { super .run(); new MyService().serviceMethod(myList, "threadB" ); } } static class MyList { private List list = new ArrayList<String>(); synchronized public void add (String data) { list.add(data); } synchronized public int getSize () { return list.size(); } } public static void main (String[] args) throws InterruptedException { MyService.MyList myList = new MyService.MyList(); MyService.ThreadA threadA = new MyService.ThreadA(myList); threadA.setName("a" ); threadA.start(); MyService.ThreadB threadB = new MyService.ThreadB(myList); threadB.setName("b" ); threadB.start(); Thread.sleep(6000 ); System.out.println("listSize=" + myList.getSize()); } }
运行结果:
结果显示出现来脏读,程序脱离来我们的本意,使用synchronized(非this对象x)
即可解决这个问题,代码修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void serviceMethod (MyList myList, String data) { try { synchronized (myList) { if (myList.getSize() < 1 ) { Thread.sleep(2000 ); myList.add(data); } } } catch (InterruptedException e) { e.printStackTrace(); } }
对synchronized(非this对象x)三个结论 synchronized(非this对象x)
格式的写法是将X对象本身作为“对象监视器”
当多个线程同时执行synchronized(非this对象x){}
同步代码块时呈同步效果
当其他线程执行x对象中synchronized
同步方法是呈同步效果
当其他线程执行x对象方法里面的synchronized(this)
代码块时也呈现同步效果
注意:如果其他线程调用不加synchronized
关键字的方法或者同步代码块时还是异步调用
静态同步synchronized方法与synchronized(class)代码块和数据烈性String的常量池性
关键字synchronized
还可以应用在static静态方法上,如果这样写,那是对当前的*.java
文件对Class
类进行持锁。它和加在非static静态方法上的和代码块的作用是一样的
在JVM中具有String常量缓存的功能,将synchronized(非this对象x)
同步块与String联合使用时,要注意String a="AA"; String b="AA"; //由于常量池特殊性 a和b是想等的
,所以在两个线程分别持有对象锁a和对象锁b时,他们其实持有的对象锁是同一个“AA”,所有在其中一个线程持有锁时,另一个线程时阻塞的无法执行相应的程序。所以在大多数的情况下,同步synchronized(非this对象x)
代码块都不使用String作为锁对象。
同步方法容易造成死循环。无论是两个或者多个synchronized
方法有关联或无关联,若其中一个synchronized
方法无限执行中,就会造成其他线程的无限等待。采用synchronized
同步代码块持有不同的锁类型即可解决。
对于class A{.....},A a=new A(),synchronized(a){}
,如果此时对象锁a未释放,而其他线程调用a的同步非静态方法,则阻塞等待,直到对象锁a释放。
锁对象的更改 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public class MyService { private String lock = "123" ; public void serviceMethod () { try { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " begin " + System.currentTimeMillis()); lock = "456" ; Thread.sleep(2000 ); System.out.println(Thread.currentThread().getName() + " end " + System.currentTimeMillis()); } } catch (InterruptedException e) { e.printStackTrace(); } } static class ThreadA extends Thread { private MyService myService; ThreadA(MyService myService) { this .myService = myService; } @Override public void run () { myService.serviceMethod(); } } static class ThreadB extends Thread { private MyService myService; ThreadB(MyService myService) { this .myService = myService; } @Override public void run () { myService.serviceMethod(); } } public static void main (String[] args) throws InterruptedException { MyService myService = new MyService(); MyService.ThreadA threadA = new MyService.ThreadA(myService); threadA.setName("A" ); MyService.ThreadB threadB = new MyService.ThreadB(myService); threadB.setName("B" ); threadA.start(); Thread.sleep(50 ); threadB.start(); } }
运行结果:
1 2 3 4 A begin 1550671515201 B begin 1550671515256 A end 1550671517203 B end 1550671517261
由于线程阻塞了50毫秒,导致ThreadB运行时持有的锁变成了“456”,而ThreadA仍然是“123”,所以两个线程呈异步。去掉睡眠50毫秒这行代码多运行几次,发现结果有时同步有时异步,并不一定,也就是说和去不去掉50毫秒的关系并不起决定行作用,只要线程ThreadB在lock变成“456”之前运行,那么结果就是同步则两个线程获取的锁都为“A”,在其之后就是异步,两个线程获取的锁分别为“123”和“456”,这也是线程运行随机性造成的。