【Java并发编程】2.1对象及变量的并发访问——synchronized同步方法

前言 着重掌握如下技术点:
1.synchronized 对象监视器为Object的使用;
2.synchronized 对象监视器为Class时的使用;
3.非线程安全时如何出现的;
4.关机字volatile的主要作用;
5.关键字volatile与synchronized的区别与使用情况。
synchronized同步方法 非线程安全会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据是被更改过的。
而线程安全就是已获得的实例变量的值是经过同步处理的,不会出现脏读的线程。
2.1.1方法内的变量为线程安全
“非线程安全”问题存在于“实例变量中”,如果是方法内部的私有变量,则不存在“非线程安全问题”,所以结果也是线程安全的。
下面例子实现方法内部声明一个变量,是不存在非线程安全问题的。

public class HasSelfPrivateNum {public void addI(String userName) { int num; if(userName.equals("a")){ num=100; System.out.println("a set off"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }else{ num=200; System.out.println("b set off"); } System.out.println(userName+" num="+num); } }class ThreadA extends Thread {private HasSelfPrivateNum numRef; public ThreadA(HasSelfPrivateNum numRef) { this.numRef = numRef; }@Override public void run() { super.run(); numRef.addI("a"); } }class ThreadB extends Thread {private HasSelfPrivateNum numRef; public ThreadB(HasSelfPrivateNum numRef) { this.numRef = numRef; }@Override public void run() { super.run(); numRef.addI("b"); } }class Run{ public static void main(String[] args) { HasSelfPrivateNum numRef = new HasSelfPrivateNum(); ThreadA threadA = new ThreadA(numRef); threadA.start(); ThreadB threadB = new ThreadB(numRef); threadB.start(); } }

结果为:
b set off b num=200 a set off a num=100

可见方法中的内部变量num所执行的操作 不存在非线程安全问题。
2.1.2实例变量非线程安全
如果多个线程共同访问一个对象中的实例变量就有可能出现“非线程安全问题”。
用线程访问的对象中如果有多个实例变量,则运行结果有可能出现交叉的情况。
如果对象中仅有一个实例变量,则有可能出现覆盖。
/** * 实例变量num被多个线程访问 * @author Arthur * @date 2017-12-25 14:15 */public class HasSelfPrivateNum {private int num =0; public void addI(String userName) { if(userName.equals("a")){ num=100; System.out.println("a set off"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }else{ num=200; System.out.println("b set off"); } System.out.println(userName+" num="+num); } }class ThreadA extends Thread {private HasSelfPrivateNum numRef; public ThreadA(HasSelfPrivateNum numRef) { this.numRef = numRef; }@Override public void run() { super.run(); numRef.addI("a"); } }class ThreadB extends Thread {private HasSelfPrivateNum numRef; public ThreadB(HasSelfPrivateNum numRef) { this.numRef = numRef; }@Override public void run() { super.run(); numRef.addI("b"); } }class Run{ public static void main(String[] args) { HasSelfPrivateNum numRef = new HasSelfPrivateNum(); ThreadA threadA = new ThreadA(numRef); threadA.start(); ThreadB threadB = new ThreadB(numRef); threadB.start(); } }

a set off b set off b num=200 a num=200

本例子是两个线程同时访问一个没用同步的的方法,如果两个线程同时操作业务对象中的实例变量,则会出现“非线程安全”问题。解决方案是只要在方法前加synchronized关键字即可。在两个线程访问同一个对象中的同步方法时一定是线程安全的。
2.1.3多个对象多个锁
public class HasSelfPrivateNum {private int num = 0; synchronizedpublic void addI(String userName) { if(userName.equals("a")){ num=100; System.out.println("a set off"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }else{ num=200; System.out.println("b set off"); } System.out.println(userName+" num="+num); } }class ThreadA extends Thread {private HasSelfPrivateNum numRef; public ThreadA(HasSelfPrivateNum numRef) { this.numRef = numRef; }@Override public void run() { super.run(); numRef.addI("a"); } }class ThreadB extends Thread {private HasSelfPrivateNum numRef; public ThreadB(HasSelfPrivateNum numRef) { this.numRef = numRef; }@Override public void run() { super.run(); numRef.addI("b"); } }class Run{ public static void main(String[] args) { HasSelfPrivateNum numRef = new HasSelfPrivateNum(); HasSelfPrivateNum numRef2 = new HasSelfPrivateNum(); ThreadA threadA = new ThreadA(numRef); threadA.start(); ThreadB threadB = new ThreadB(numRef2); threadB.start(); } }

a set off b set off b num=200 a num=100

上述代码由于创建了2个HasSelfPrivateNum 实例,拥有了两个锁,虽然方法是同步的,但是造成的结果却是异步的。效果是先打印出b的值 再打印出a的值。
为什么是这样的呢?synchronized关键字是对对象加锁,而不是对方法或代码块加锁,上述例子中哪个线程先执行带synchronized的方法,哪个线程就先获得该方法所属对象的锁Lock,其他线程等待,前提是多个线程访问的是同一个对象。
但如果多个线程访问多个对象,JVM就会创建多个锁。上述问题就是如此。
2.1.4synchronized方法与锁对象
为了验证线程锁的是对象,代码如下:
/** * 验证线程锁的是对象 * * @author Arthur * @date 2017-12-26 16:08 */public class SynchronizedMethodLock { public static void main(String[] args) { MyObject myObject = new MyObject(); MyThreadA myThreadA = new MyThreadA(myObject); MyThreadB myThreadB = new MyThreadB(myObject); myThreadA.setName("A"); myThreadB.setName("B"); myThreadA.start(); myThreadB.start(); } }class MyObject{ public void method() throws InterruptedException { System.out.println("begin threadName:"+Thread.currentThread().getName()); Thread.sleep(5000); System.out.println("end"); } } class MyThreadA extends Thread{private MyObject myObject; public MyThreadA(MyObject myObject) { this.myObject = myObject; }@Override public void run() { super.run(); try { myObject.method(); } catch (InterruptedException e) { e.printStackTrace(); } } }class MyThreadB extends Thread{private MyObject myObject; public MyThreadB(MyObject myObject) { this.myObject = myObject; }@Override public void run() { super.run(); try { myObject.method(); } catch (InterruptedException e) { e.printStackTrace(); } } }

运行结果为
begin threadName:A begin threadName:B A end B end

当method()方法用synchronized修饰时
public synchronized void method()

运行结果为
begin threadName:A A end begin threadName:B B end

可见调用关键字synchronized声明的方法一定是排队运行的。另外只有共享的资源的读写访问才需要同步化处理!
那其他方法在被调用时是什么效果呢?如何查看Lock锁对象的效果呢?更新代码如下:
/** * 验证线程锁的是对象 * * @author Arthur * @date 2017-12-26 16:08 */public class SynchronizedMethodLock { public static void main(String[] args) { MyObject myObject = new MyObject(); MyThreadA myThreadA = new MyThreadA(myObject); MyThreadB myThreadB = new MyThreadB(myObject); myThreadA.setName("A"); myThreadB.setName("B"); myThreadA.start(); myThreadB.start(); } }class MyObject{ public /*synchronized*/ void method() throws InterruptedException { System.out.println("begin threadName:"+Thread.currentThread().getName()); Thread.sleep(5000); System.out.println(Thread.currentThread().getName()+" end"); } } class MyThreadA extends Thread{private MyObject myObject; public MyThreadA(MyObject myObject) { this.myObject = myObject; }@Override public void run() { super.run(); try { myObject.method(); } catch (InterruptedException e) { e.printStackTrace(); } } }class MyThreadB extends Thread{private MyObject myObject; public MyThreadB(MyObject myObject) { this.myObject = myObject; }@Override public void run() { super.run(); try { myObject.method(); } catch (InterruptedException e) { e.printStackTrace(); } } }

结果如下
begin threadName:A begin threadName:B A end B end

可以看到即使methodA为同步方法,线程A持有了myObject对象的锁,但是线程B仍能够异步调用非synchronized方法methodB。
继续试验,在methodB方法上也加上synchronized
public synchronized void methodB()

运行结果为按顺序执行。
begin methodA threadName:A A end begin methodB threadName:B B end

以此可以得出结论
1)A线程先持有object对象的锁,B线程可以以异步的方法调用object对象中非synchronized的方法。
2)A线程先持有object对象的锁,B线程如果这时调用object对象中的synchronized的方法那么需要等待,也就是同步。
2.1.5脏读
前面说明了在多个线程调用同一方法时,为了避免数据交叉的情况,用synchroinized同步。但是有些操作虽然在赋值时进行了同步,但在取值的时候可能出现意外,这种情况就是脏读。发生脏读的情况是在读取实例变量的时候,此值已经被其他线程更改过了。
/** * 脏读现象 * * @author Arthur * @date 2017-12-26 17:07 */public class Run { public static void main(String[] args) throws InterruptedException { PublicVar varRef = new PublicVar(); ThreadA threadA = new ThreadA(varRef); threadA.start(); threadA.setName("threadA"); Thread.sleep(200); //打印结果受此值大小影响 varRef.getValue(); } } class PublicVar{ public String username = "A"; public String password = "AA"; synchronized public void setValue(String username, String password) { try { this.username = username; Thread.sleep(5000); this.password = password; System.out.println("setValue method threadName="+Thread.currentThread().getName()+" username="+username+" password="+password); } catch (InterruptedException e) { e.printStackTrace(); } }public void getValue() { System.out.println("getValue method threadName="+Thread.currentThread().getName()+" username="+username+" password="+password); } }class ThreadA extends Thread{ private PublicVar var; public ThreadA(PublicVar var) { this.var = var; }@Override public void run() { super.run(); var.setValue("B","BB"); } }

程序运行为:
getValue method threadName=main username=B password=AA setValue method threadName=threadA username=B password=BB

出现脏读是因为public void getValue()方法不是同步的,所以在任意时刻都可以调用。
解决方案是加上同步关键字
synchronized public void getValue() { System.out.println("getValue method threadName="+Thread.currentThread().getName()+" username="+username+" password="+password); }

程序运行正常,setValue和getValue被依次执行。
setValue method threadName=threadA username=B password=BB getValue method threadName=main username=B password=BB

小结:
1.当A线程调用anyObject的synchronized的X方法时,就取得了X方法锁,更准确的来说是anyObject的对象锁,所以其他线程必须等A线程执行完X方法才能调用X方法,但是其他线程可以所以调用非synchronized的方法。
2.当A线程调用anyObject的synchronized的X方法时,就取得了X方法锁,更准确的来说是anyObject的对象锁,所以其他线程必须等A线程执行完X方法才能调用X方法,但是如果其他线程B调用声明了synchronized的非X方法时,由于对象锁被A线程持有,必须等A线程执行完X方法后,才能执行synchronized的非X方法。这种情况不会出现脏读现象。
2.1.6锁重入
synchronized拥有锁重入功能,通俗的讲当某个线程获得对象锁以后,在此请求此对象锁可以在此得到该对象的锁。这也证明在一个synchronized方法/块内部调用本类的其他synchronized方法/块是永远可以得到锁的。
看代码:
/** * 锁重入 * @author Arthur * @date 2017-12-26 17:25 */public class Run { public static void main(String[] args) { ThreadA threadA = new ThreadA(); threadA.start(); } }class Service{public synchronized void service1() { System.out.println("service1."); service2(); }public synchronized void service2(){ System.out.println("service2."); service3(); }public synchronized void service3(){ System.out.println("service3."); }}class ThreadA extends Thread{@Override public void run() { super.run(); Service service = new Service(); service.service1(); } }

结果为
service1. service2. service3.

可重入的概念:当一个线程持有对象锁时,此时这个对象锁还未被释放,当它再次请求获得锁时还是可以获得。如果不能重入就会造成死锁。
重入锁也支持在父子类中调用。
2.1.7出现异常,锁自动释放
当一个线程执行异常时,其所持有的锁自动释放。
2.1.8同步不具有继承性
/** * 同步不具有继承性 * * @author Arthur * @date 2017-12-26 17:37 */public class Run { public static void main(String[] args) { Sub subRef = new Sub(); ThreadA threadA = new ThreadA(subRef); threadA.setName("A"); threadA.start(); ThreadB threadB = new ThreadB(subRef); threadB.setName("B"); threadB.start(); } }class Main{ synchronized public void sleep(){ try { System.out.println(" Main 下一步 sleep begin thread name="+Thread.currentThread().getName()+" time="+System.currentTimeMillis()); Thread.sleep(5000); System.out.println(" Main 下一步 sleep end thread name="+Thread.currentThread().getName()+" time="+System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } }class Sub extends Main{ @Override public void sleep(){ super.sleep(); try { System.out.println(" Sub 下一步 sleep begin thread name="+Thread.currentThread().getName()+" time="+System.currentTimeMillis()); Thread.sleep(5000); System.out.println(" Sub 下一步 sleep end thread name="+Thread.currentThread().getName()+" time="+System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } }class ThreadA extends Thread{private Sub sub; public ThreadA(Sub sub) { this.sub = sub; }@Override public void run() { super.run(); sub.sleep(); } }class ThreadB extends Thread{private Sub sub; public ThreadB(Sub sub) { this.sub = sub; }@Override public void run() { super.run(); sub.sleep(); } }

运行结果:
Main 下一步 sleep begin thread name=A time=1514281689722 Main 下一步 sleep end thread name=A time=1514281694722 Main 下一步 sleep begin thread name=B time=1514281694722 Sub 下一步 sleep begin thread name=A time=1514281694722 Main 下一步 sleep end thread name=B time=1514281699723 Sub 下一步 sleep begin thread name=B time=1514281699723 Sub 下一步 sleep end thread name=A time=1514281699723 Sub 下一步 sleep end thread name=B time=1514281704723

【【Java并发编程】2.1对象及变量的并发访问——synchronized同步方法】由结果可看出同步未被继承。在子类的方法加上synchronized
public synchronized void sleep()

程序运行正常:
Main 下一步 sleep begin thread name=B time=1514281920201 Main 下一步 sleep end thread name=B time=1514281925201 Sub 下一步 sleep begin thread name=B time=1514281925201 Sub 下一步 sleep end thread name=B time=1514281930201 Main 下一步 sleep begin thread name=A time=1514281930201 Main 下一步 sleep end thread name=A time=1514281935202 Sub 下一步 sleep begin thread name=A time=1514281935202 Sub 下一步 sleep end thread name=A time=1514281940202

下一节介绍2.2 synchronized同步语句块

    推荐阅读