synchronized
方法内变量是线程安全
关于“线程安全”和“非线程安全”相关的技术点,是学习多线程一定会遇到的经典问题。“非线程安全”通常发生在多个线程对同一个对象的实例变量进行并发访问。产生的后果就是“脏读”,也就是读取到的数据其实是被其他线程更改过的。synchronized 通过同步互斥来解决这个问题。
注意,“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量。那么不存在“非线程安全”问题。所得到的结果自然就是“线程安全”的。
package com.skystep.thread.threadsafe;
public class HasSelfPrivateNum {
public void addI(String username) {
try{
int num = 0;
if (username.equals("a")) {
num = 100;
System.out.println("a set over");
Thread.sleep(2000);
}else {
num = 200;
System.out.println("b set over");
}
System.out.println(username + " num=" + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package com.skystep.thread.threadsafe;
public class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
this.numRef.addI("a");
}
}
package com.skystep.thread.threadsafe;
public class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
this.numRef.addI("b");
}
}
package com.skystep.thread.threadsafe;
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
ThreadA a = new ThreadA(hasSelfPrivateNum);
a.start();
ThreadB b = new ThreadB(hasSelfPrivateNum);
b.start();
}
}
执行结果:
a set over
b set over
b num=200
a num=100
其中 num 为方法内变量,两个线程如期望输出结果。可见,方法中的变量不存在非线程安全问题,永远是线程安全的,因为方法内部的变量是私有造成的。
实例变量是非线程安全
如果多个线程并发访问同一个对象的某个实例变量,便可能发生“非线程安全”问题,线程访问的对象如果有多个实例变量,则运行结果有可能出现交叉的情况。如果对象只有一个实例变量,则有可能出现覆盖的情况。
package com.skystep.thread.threadunsafe;
public class HasSelfPrivateNum {
private int num = 0;
public void addI(String username) {
try{
if (username.equals("a")) {
num = 100;
System.out.println("a set over");
Thread.sleep(2000);
}else {
num = 200;
System.out.println("b set over");
}
System.out.println(username + " num=" + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package com.skystep.thread.threadunsafe;
public class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
this.numRef.addI("a");
}
}
package com.skystep.thread.threadunsafe;
public class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
this.numRef.addI("b");
}
}
package com.skystep.thread.threadunsafe;
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
ThreadA a = new ThreadA(hasSelfPrivateNum);
a.start();
ThreadB b = new ThreadB(hasSelfPrivateNum);
b.start();
}
}
执行结果:
a set over
b set over
b num=200
a num=200
两个线程同时访问对象中实例变量。便出现了“非线程安全问题”。对于此类问题,早在 JAVA 1.6时便提供了 synchronized 同步互斥锁来解决此问题。
synchronized 同步方法
在 Java 语言中,每一个对象都会内置一把锁(监视器锁),这是一种互斥锁,每一时间片只能有一个线程获取该锁,其他线程进入等待,直到原先线程释放锁。 线程可以使用 synchronized 关键字来获取对象上的锁。synchronized 关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁)。
package com.skystep.thread.sync;
public class HasSelfPrivateNum {
private int num = 0;
public synchronized void addI(String username) {
try {
if (username.equals("a")) {
num = 100;
System.out.println("a set over");
Thread.sleep(2000);
} else {
num = 200;
System.out.println("b set over");
}
System.out.println(username + " num=" + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package com.skystep.thread.sync;
public class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
this.numRef.addI("a");
}
}
package com.skystep.thread.sync;
public class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
this.numRef.addI("b");
}
}
package com.skystep.thread.sync;
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
ThreadA a = new ThreadA(hasSelfPrivateNum);
a.start();
ThreadB b = new ThreadB(hasSelfPrivateNum);
b.start();
}
}
执行结果:
a set over
a num=100
b set over
b num=200
通过对 addI 方法添加 synchronized 关键字修饰之后,该程序重新符合预期输出结果。当 ThreadA 线程获取到 hasSelfPrivateNum 对象锁时,ThreadB 会进入等待,直到 ThreadA 执行完 addI 方法。这就是同步互斥。
synchronized 错误使用 多个对象多个锁
从前面例子来看,关键字 synchronized 取得的锁都是对象锁,而不是一段代码或者函数,不同线程必须竞争同一把锁,才能达到同步互斥的目的。但是对于初学者常常会出现使用错误。出现多个对象多把锁的问题。
package com.skystep.thread.syncerroruse;
public class HasSelfPrivateNum {
private int num = 0;
public synchronized void addI(String username) {
try {
if (username.equals("a")) {
num = 100;
System.out.println("a set over");
Thread.sleep(2000);
} else {
num = 200;
System.out.println("b set over");
}
System.out.println(username + " num=" + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package com.skystep.thread.syncerroruse;
public class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
this.numRef.addI("a");
}
}
package com.skystep.thread.syncerroruse;
public class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
this.numRef.addI("b");
}
}
package com.skystep.thread.syncerroruse;
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum hasSelfPrivateNumA = new HasSelfPrivateNum();
ThreadA a = new ThreadA(hasSelfPrivateNumA);
a.start();
HasSelfPrivateNum hasSelfPrivateNumB = new HasSelfPrivateNum();
ThreadB b = new ThreadB(hasSelfPrivateNumB);
b.start();
}
}
执行结果:
a set over
b set over
b num=200
a num=100
从例子来看,main 线程创建了多个线程。ThreadA 线程使用的是 hasSelfPrivateNumA 对象作为锁,ThreadB 使用 hasSelfPrivateNumB 对象锁,并不能达到同步互斥的目标。
synchronized 方法和锁对象 锁住的是谁
使用 synchronized 修饰函数,便是将函数调用者实例作为对象锁。不同线程调用被 synchronized 修饰的函数,必须抢占该对象锁,谁先抢到谁先执行函数代码,抢不到的线程进入阻塞状态,直到锁被释放。
package com.skystep.thread.syncerroruse;
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum hasSelfPrivateNumA = new HasSelfPrivateNum();
ThreadA a = new ThreadA(hasSelfPrivateNumA);
a.start();
HasSelfPrivateNum hasSelfPrivateNumB = new HasSelfPrivateNum();
ThreadB b = new ThreadB(hasSelfPrivateNumB);
b.start();
}
}
执行结果:
a set over
b set over
b num=200
a num=100
ThreadA 执行时 执行函数 run 调用了 hasSelfPrivateNumA对象的 addI 方法,抢占锁对象是 hasSelfPrivateNumA,而 ThreadB 执行时 执行函数 run 调用了 hasSelfPrivateNumB对象的 addI 方法,抢占的锁是 hasSelfPrivateNumB 。
synchronized 锁重入 无限套娃不违法
关键字 synchronized 拥有重入锁的功能 。当一个线程获取到对象锁时,再次请求此对象锁时是可以再次得到该对象的锁的。JAVA 中称为锁重入。是同一锁,才有重入一说,套的娃必须是同一个。
重入锁的概念是:自己可以再次获取自己的内部锁,比如线程 A 获得某个对象的锁,此时锁没有释放,当其再次获取这个对象的锁的时候可以获得。如果不可的重入的锁,就会造成死锁。
synchronized 锁释放 遇异常自动释放
ThreadA 执行时 执行函数 run 调用了 hasSelfPrivateNumA对象的 addI 方法,抢占锁对象是 hasSelfPrivateNumA,而 ThreadB 执行时 执行函数 run 调用了 hasSelfPrivateNumB对象的 addI 方法,抢占的锁是 hasSelfPrivateNumB 。
synchronized 同步代码块
被 synchronized 关键字修饰的语句块会自动被加上内置锁,从而实现同步。
public class Counter {
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public void add() {
synchronized (this){
this.num++;
}
}
}
public class Sync {
public static void main(String[] args) {
Counter counter = new Counter();
counter.setNum(0);
Thread[] threds = new Thread[2];
for (int i = 0; i < 2; i++) {
threds[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 2000; j++) {
counter.add();
}
}
});
}
threds[0].start();
threds[1].start();
try {
threds[0].join();
threds[1].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num:" + counter.getNum());
}
}
执行结果:
num:4000
在代码段 num++ 中添加 synchronized 修饰,便完成共有变量 num 的读写同步。
简述 synchronized 实现原理
在多线程并发编程中 synchronized 一直是元老级的存在。很多人称之为重量级锁,但在 JAVA SE1.6 进行了各种优化之后,已经没有那么重量级了。为了减少获取锁和释放锁带来的消耗而引进了偏向锁和轻量级锁。
JAVA 中的每一个对象都可以作为锁,具体表象有如下三种:
- 对于普通同步方法,锁是当前的实例对象;
- 对于静态同步方法,锁是当前类的 Class 对象;
- 对于同步方法快,锁的是括号里配置的对象;
在 JVM 规范中可以知道,JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步。monitorenter 指令在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处或者异常的地方,JVM 保证了每个 monitorenter 都有与之对应的 monitorexit。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有之后,它将处于锁定状态。线程执行到 monitorexit 指令后,将会尝试获取对象对应的 monitor 的所有权,即尝试获得对象的锁。