对象和变量的并发访问synchronized解析以及死锁分析排查
一.synchronized
java并发编程中存在“非线程安全"问题。“非线程安全"是指发生在多个线程对同一个对象中的实例变量并发访问时,产生的”脏读“现象,使用synchronized同步处理可解决这一问题。非线程安全问题存在于实例变量中,不存在方法内部的私有变量。
1、synchronized修饰方法的两种情况:
(1).当A线程调用某个对象的synchronized方法,先持有某个对象的锁;这时B线程需要等待A线程执行完毕后释放这个对象锁才可调用这个对象的synchronized方法,即同步。synchronized是一个独占锁,每个锁请求之间是互斥的
(2).当A线程调用某个对象的synchronized方法时,B线程调用这个对象的其他非synchronized方法,不需要等待。
下面是上面两种结论的证明代码:
/** * @author monkjavaer * @date 2018/11/26 21:12 */ public class Service2 { /** * 同步方法 */ public synchronized void printService() { System.out.println(Thread.currentThread().getName() + " " + "start printService thread"); try { TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " " + "printService end "); } /** * 同步方法 */ public synchronized void printServiceOther() { System.out.println(Thread.currentThread().getName() + " " + "start printServiceOther thread"); try { TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " " + "printServiceOther end "); } /** * 非同步方法 */ public void printServiceNotSynchronized() { System.out.println(Thread.currentThread().getName() + " " + "start printServiceNotSynchronized thread"); try { TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " " + "printServiceNotSynchronized end "); } }
线程A:
public class Syn2ThreadA extends Thread{ private Service2 service; public Syn2ThreadA(Service2 service) { this.service = service; } @Override public void run() { super.run(); service.printService(); } }
线程B:
public class Syn2ThreadB extends Thread{ private Service2 service; public Syn2ThreadB(Service2 service) { this.service = service; } @Override public void run() { super.run(); service.printServiceNotSynchronized(); } }
线程C:
public class Syn2ThreadC extends Thread{ private Service2 service; public Syn2ThreadC(Service2 service) { this.service = service; } @Override public void run() { super.run(); service.printServiceOther(); } }
测试方法:
public class Syn2Test { public static void main(String[] args) { Service2 service = new Service2(); //Syn2ThreadA调用了同步方法 Syn2ThreadA threadA = new Syn2ThreadA(service); threadA.setName("threadA"); //Syn2ThreadB调用非同步 Syn2ThreadB threadB = new Syn2ThreadB(service); threadB.setName("threadB"); //Syn2ThreadC调用了同步方法 Syn2ThreadC threadC = new Syn2ThreadC(service); threadC.setName("threadC"); threadA.start(); threadB.start(); threadC.start(); } }
可通过执行上面代码通过程序输出顺序证明以上结论的正确性。
2、synchronized重入
可重入锁:即某个线程可以获得一个它自己已持有的锁。下面的例子在继承关系中子类可以通过可重入锁调用父类的同步方法,提升了加锁行为的封装性。如果没有可重入锁就会产生死锁。
父类:
public class Fruit { public synchronized void dosomething(){ System.out.println("printFruit"); } }
子类:
public class Apple extends Fruit { @Override public synchronized void dosomething() { super.dosomething(); System.out.println("apple"); } }
3.死锁
那什么是死锁呢? 下面是维基百科对死锁的定义:
死锁(英语:Deadlock),又译为死结,计算机科学名词。当两个以上的运算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出时,就称为死锁。
死锁的四个条件是:
禁止抢占 no preemption - 系统资源不能被强制从一个进程中退出
持有和等待 hold and wait - 一个进程可以在等待时持有系统资源
互斥 mutual exclusion - 只有一个进程能持有一个资源
循环等待 circular waiting - 一系列进程互相持有其他进程所需要的资源
死锁只有在这四个条件同时满足时出现。预防死锁就是至少破坏这四个条件其中一项,即破坏“禁止抢占”、破坏“持有等待”、破坏“资源互斥”和破坏“循环等待”。
下面这张图片清除的描述了死锁的发生:
编码证明:
/** * @author monkjavaer * @date 2018/11/26 22:37 */ public class TestDeadlock { static final String resource1 = "resource1"; static final String resource2 = "resource2"; static class ThreadA extends Thread { @Override public void run() { synchronized (resource1) { System.out.println("ThreadA: locked resource 1"); try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (resource2) { System.out.println("ThreadA: locked resource 2"); } } } } static class ThreadB extends Thread { @Override public void run() { synchronized (resource2) { System.out.println("ThreadB: locked resource 2"); try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (resource1) { System.out.println("ThreadB: locked resource 1"); } } } } public static void main(String[] args) { ThreadA threadA = new ThreadA(); threadA.setName("====ThreadA===="); ThreadB threadB = new ThreadB(); threadB.setName("====ThreadB===="); threadA.start(); threadB.start(); } }
上面的代码运行时会产生死锁情况。
4.追踪、分析死锁发生
死锁检查方法,命令窗口运行:
1、jps
2、jstack -l 端口 //-l 选项用于打印锁的附加信息
下面是部分死锁信息:
死锁只有在这四个条件同时满足时出现。预防死锁就是至少破坏这四个条件其中一项,即破坏“禁止抢占”、破坏“持有等待”、破坏“资源互斥”和破坏“循环等待”。
5、最后是部分synchronized知识点补充:
a、synchronized同步方法弊端:如果一个线程调用同步方法要执行很长时间,那么其他线程要调用这个同步方法必须等待很长时间。
可以用synchronized解决,不在synchronized块中的代码异步执行。并且synchronized块synchronized方法是一样的使用的对象监视器是一个,他们都是锁定的当前对象。
b、synchronized同步不能被继承。
c、synchronized加到静态方法上时是给class类上锁,加到非静态方法是给对象上锁。
d、一般synchronized块不用String作为锁对象,因为String常量池有缓存功能。导致锁对象可能一直是相同的。
String a = "a";
String b = "a";
System.out.println(a==b);
比如上面的代码得到的结果是true