使用synchronized关键字声明方法有些时候是有很大的弊端的,比如我们有两个线程一个线程A调用同步方法后获得锁,那么另一个线程B就需要等待A执行完,但是如果说A执行的是一个很费时间的任务的话这样就会很耗时。
synchronized(this)同步代码块
当两个并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块才能执行该代码块。
一个线程访问object的一个同步代码块时,另外一个线程仍然可以访问该对象的非同步代码块。 以下代码可以证明:不在synchronized块中就是异步执行,在synchronized块中就是同步执行。
class Task{ public void doLongTimeTask(){ for (int i=0;i<5;i++){ System.out.println("nonsync threadName:"+Thread.currentThread().getName()+" i: "+i); } System.out.println(); synchronized (this){ for (int i=0;i<5;i++){ System.out.println("sync threadName:"+Thread.currentThread().getName()+" i: "+i); } } }}class MyThread extends Thread{ private Task task; public MyThread(Task task){ this.task=task; } @Override public void run() { task.doLongTimeTask(); }}public class Run { public static void main(String[] args) { Task task=new Task(); MyThread t1=new MyThread(task); MyThread t2=new MyThread(task); t1.start(); t2.start(); }}复制代码
运行结果为:
nonsync threadName:Thread-0 i: 0nonsync threadName:Thread-1 i: 0nonsync threadName:Thread-0 i: 1nonsync threadName:Thread-1 i: 1nonsync threadName:Thread-1 i: 2nonsync threadName:Thread-1 i: 3nonsync threadName:Thread-0 i: 2nonsync threadName:Thread-1 i: 4nonsync threadName:Thread-0 i: 3nonsync threadName:Thread-0 i: 4sync threadName:Thread-1 i: 0sync threadName:Thread-1 i: 1sync threadName:Thread-1 i: 2sync threadName:Thread-1 i: 3sync threadName:Thread-1 i: 4sync threadName:Thread-0 i: 0sync threadName:Thread-0 i: 1sync threadName:Thread-0 i: 2sync threadName:Thread-0 i: 3sync threadName:Thread-0 i: 4复制代码
可以看到,非同步代码块是异步执行的,同步代码块则是排队执行的。
同步代码块之间的同步性:在使用synchronized(this)代码块时,当一个线程访问object的一个同步代码块时,其他线程对同一个对象中所有其他synchronized(this)代码块的访问会被阻塞,说明synchronized使用的“对象监视器是一个”。
synchronized(this)代码块是锁定当前对象的。也就是说,多个线程调用同一个对象的不同名称的synchronized同步方法或者synchronized(this)同步代码块时,调用的效果就是按顺序执行,就是同步的,阻塞的。也就是说,无论是同步方法或者synchronized(this)代码块,都可以对其他同步方法或代码块调用呈阻塞状态。同一时间只有一个线程可以执行同步方法或同步代码块中的代码。
synchronized(非this对象)代码块
除了synchronized(this)同步代码块,Java还支持对任意对象作为“对象监视器”来实现同步的功能。任意对象大多数是实例变量及方法的参数。同样的,在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this)同步代码块中的代码。
锁非this对象有一定的优点:如果在一个类中有很多synchronized方法,这时虽然能实现同步,但会收到阻塞,影响运行效率。但是如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,可以提高运行效率。
在使用synchronized(非this对象)同步代码块时,对象监视器必须是同一个对象,如果不是同一个对象监视器,运行的结果就是异步调用了。示例代码:
package ch02.t12;class Service { private String anyStr = new String(); public void method() { try { synchronized (anyStr) { System.out.println("Thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + "进入同步块"); Thread.sleep(3000); System.out.println("Thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + "离开同步块"); } } catch (InterruptedException e) { e.printStackTrace(); } }}class MyThread extends Thread{ private Service service; public MyThread(Service service){ this.service=service; } @Override public void run() { service.method(); }}public class Run { public static void main(String[] args) { Service service=new Service(); MyThread a=new MyThread(service); a.setName("A"); a.start(); MyThread b=new MyThread(service); b.setName("B"); b.start(); }}复制代码
运行结果为:
Thread: A time: 1544337130842进入同步块Thread: A time: 1544337133842离开同步块Thread: B time: 1544337133842进入同步块Thread: B time: 1544337136852离开同步块复制代码
如果将Service改成:
class Service { public void method() { try { String anyStr = new String(); synchronized (anyStr) { System.out.println("Thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + "进入同步块"); Thread.sleep(3000); System.out.println("Thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + "离开同步块"); } } catch (InterruptedException e) { e.printStackTrace(); } }}复制代码
运行结果为:
Thread: B time: 1544337317933进入同步块Thread: A time: 1544337317933进入同步块Thread: A time: 1544337320933离开同步块Thread: B time: 1544337320933离开同步块复制代码
是异步进行的,因此此时两个anyStr对象不是同一个对象了。
synchronized代码块的“脏读”
同步代码块放在非同步方法中进行生命,并不能摆正调用方法的线程的执行同步、顺序性。线程调用方法的顺序是无序的,虽然在同步块中执行的顺序是同步的,这样容易出现“脏读”问题。
class MyOneList{ private ArrayList list=new ArrayList(); synchronized public void add(String data){ list.add(data); } synchronized public int getSize(){ return list.size(); }}class MyService{ public MyOneList add(MyOneList list,String data){ try { System.out.println(Thread.currentThread().getName()); if(list.getSize()<1){ //保证list只有一个元素 Thread.sleep(2000); list.add(data); } } catch (InterruptedException e) { e.printStackTrace(); } return list; }}class MyThread extends Thread{ private MyOneList list; public MyThread(MyOneList list){ this.list=list; } @Override public void run() { MyService service=new MyService(); service.add(list,"A"); }}public class Run { public static void main(String[] args) throws InterruptedException { MyOneList list=new MyOneList(); MyThread t1=new MyThread(list); t1.setName("A"); t1.start(); MyThread t2=new MyThread(list); t2.setName("B"); t2.start(); Thread.sleep(6000); System.out.println(list.getSize()); }}复制代码
运行结果:
BA2复制代码
首先可以发现,两个线程打印的顺序是无序的,说明线程的执行时异步的。返回的结果中list的size为2,原因是在add()方法中,list.getSize()这一方法是异步调用的。因此,需要对add()方法进行同步化,修改如下:
class MyService { public MyOneList add(MyOneList list, String data) { try { synchronized (list) { System.out.println(Thread.currentThread().getName()); if (list.getSize() < 1) { //保证list只有一个元素 Thread.sleep(2000); list.add(data); } } } catch (InterruptedException e) { e.printStackTrace(); } return list; }}复制代码
list对象在项目中只有一份实例,且对其进行调用,因此以list参数进行同步处理。结果如下:
AB1复制代码
synchronized(非this对象x)的三个结论
- 当多个线程同时执行synchronized(x)同步代码块时是同步的;
- 当线程A执行以x为锁的代码块时,其他线程执行x对象中的同步方法时也是同步的;
- 当线程A执行以x为锁的代码块时,其他线程执行x对象中的synchronized(this)代码块时也是同步的。
静态synchronized方法与synchronized(class)代码块
关键字synchronized还可以应用在static静态方法上,这样是堆当前的*.java文件对应的Class类加锁。
class Service{ synchronized public static void printA(){ try { System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 进入printA方法"); Thread.sleep(3000); System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 离开printA方法"); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized public static void printB(){ System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 进入printB方法"); System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 离开printB方法"); } synchronized public void printC(){ System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 进入printC方法"); System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 离开printC方法"); }}class ThreadA extends Thread{ private Service service; public ThreadA(Service service){ this.service=service; } @Override public void run() { service.printA(); }}class ThreadB extends Thread{ private Service service; public ThreadB(Service service){ this.service=service; } @Override public void run() { service.printB(); }}class ThreadC extends Thread{ private Service service; public ThreadC(Service service){ this.service=service; } @Override public void run() { service.printC(); }}public class Run { public static void main(String[] args) { Service service=new Service(); ThreadA a=new ThreadA(service); a.setName("A"); a.start(); ThreadB b=new ThreadB(service); b.setName("B"); b.start(); ThreadC c=new ThreadC(service); c.setName("C"); c.start(); }}复制代码
运行结果:
thread: A time: 1544341920925 进入printA方法thread: C time: 1544341920925 进入printC方法thread: C time: 1544341920925 离开printC方法thread: A time: 1544341923925 离开printA方法thread: B time: 1544341923925 进入printB方法thread: B time: 1544341923925 离开printB方法复制代码
可以看出,A与C并不是同步的,A与B是同步的。原因就是A和B是持有的同一个Class锁,而C持有的是对象锁。
但是,Class锁可以对类的所有对象实例起作用。修改上述的代码,只修改main方法如下:
public class Run { public static void main(String[] args) { Service service1=new Service(); Service service2=new Service(); ThreadA a=new ThreadA(service1); a.setName("A"); a.start(); ThreadB b=new ThreadB(service2); b.setName("B"); b.start(); }}复制代码
运行结果如下:
thread: A time: 1544342122668 进入printA方法thread: A time: 1544342125668 离开printA方法thread: B time: 1544342125668 进入printB方法thread: B time: 1544342125668 离开printB方法复制代码
可以看到,线程A和线程B虽然调用的是不同的对象,但是由于二者是Class锁,因此仍然是同步进行的。
synchronized(class)代码块和synchronized static方法的作用是一样的,修改上述的printA()和printB()方法如下:
class Service { public static void printA() { synchronized (Service.class) { try { System.out.println("thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + " 进入printA方法"); Thread.sleep(3000); System.out.println("thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + " 离开printA方法"); } catch (InterruptedException e) { e.printStackTrace(); } } } synchronized public static void printB() { synchronized (Service.class) { System.out.println("thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + " 进入printB方法"); System.out.println("thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + " 离开printB方法"); } }}复制代码
Run类和刚刚修改后的两个不同的Service对象一样,运行结果为:
thread: A time: 1544342263970 进入printA方法thread: A time: 1544342266970 离开printA方法thread: B time: 1544342266970 进入printB方法thread: B time: 1544342266970 离开printB方法复制代码
可以看到效果是一样的。
synchronized(string)同步代码块
在JVM中,具有String常量池缓存功能。在将synchronized(string)使用时,需要注意常量池带来的一些例外。在大多数情况下,同步代码块不推荐使用String作为锁对象,而改用其他,比如new Object(),它不放入缓存中。
多线程的死锁
死锁的场景一般是:线程 A 和线程 B 都在互相等待对方释放锁,或者是其中某个线程在释放锁的时候出现异常如死循环之类的。这时就会导致系统不可用。
常用的解决方案如下:
- 尽量一个线程只获取一个锁。
- 一个线程只占用一个资源。
- 尝试使用定时锁,至少能保证锁最终会被释放。
内置类与静态内置类
判断是否同步的方法是一样的,都是判断是否是同一个对象为锁。
锁对象的改变
如果锁对象本身改变了(如String从“a”变成了“b”),则改变前和改变后,线程的锁不一样,是异步的。但是如果锁的属性改变了,但是锁对象本身没变,则仍然是同步的。
参考资料
- 高洪岩. Java多线程编程核心技术[M]. 机械工业出版社, 2015.