对于多个线程同时访问同一个变量(即共享数据的情况),例如实现投票功能的软件时,多个线程可以同时处理同一个人的票数。那么一定会出现非线程安全的问题。
所谓“非线程安全”,主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。
通常时我们会采取synchronized来解决。synchronized可以在任意对象及方法上加锁,而加锁的这段代码被称为“互斥区”或“临界区”。
一、synchronized的作用
- 确保线程互斥的访问同步代码块
- 保证共享变量的修改能够及时可见
- 有效解决重排序问题
二、synchronized加锁原理
当一个线程想要执行同步方法里的代码时,线程首先尝试去拿这把锁,如果能够拿到这把锁,那么这个线程可以执行synchronized里面的代码。如果不能拿到这把锁,那么线程就会不断的尝试拿这把锁,直到能够拿到为止,而且是有多个线程同时去争抢这把锁。
实际上,每个对象都有一个监视器锁(monitor),而synchronized加锁之所以能够实现同步,主要是通过底层JVM通过monitor来实现的。
synchronized同步策略从用法上又可以分为代码块同步和方法同步,两者在原理上又有些差别。
1. 代码块同步
主要是通过执行monitor指令:
(1)monitorenter指令
- 如果monitor的进入数是0,则线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
- 如果其他线程已经占有了monitor,则该线程进入阻塞线程,直到monitor的进入数为0,再重新尝试获取monitor的所有权
(2)monitorexit指令
- 执行monitorexit指令的线程必须是该monitor的所有者
- 执行命令时,monitor的进入数减1变为0,则线程退出monitor,不再是monitor的所有者
2. 方法同步
主要是通过设置ACC-SYNCHRONIZED标识符:
一旦设置了标识符,执行线程将先获取monitor,获取成功后才能执行方法体,方法执行后再释放monitor。在方法执行期间,其他任何线程都无法再获取同一个monitor对象。
三、synchronized的用法
synchronized主要由以下几种形式:synchronized声明、synchronized(this)代码块、synchronized(非this)代码块、synchronized静态方法、synchronized(Class)代码块,几种形式用法各不相同,实现的功能上也有细微差别。
1. synchronized声明
synchronized最原始用法,使用非常简单,只需在需要同步的方法前添加synchronized关键字即可。但是synchronized取得的锁是对象锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态。
前提是多个线程访问的是同一个对象。如果多个线程访问多个对象,则JVM会创建多个锁。
例如:
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) {
// TODO: handle exception
e.printStackTrace();
}
}
}
public class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
// TODO Auto-generated method stub
numRef.addI("a");
}
}
ThreadB同ThreadA
同一把锁:
public class Run {
public static void main(String[] args) {
// TODO Auto-generated method stub
HasSelfPrivateNum numRef = new HasSelfPrivateNum();
ThreadA aThreadA = new ThreadA(numRef);
aThreadA.start();
ThreadB bThreadB = new ThreadB(numRef);
bThreadB.start();
}
}
运行结果:
a set over!
a num=100
b set over!
b num=200
非同一把锁:
public class Run {
public static void main(String[] args) {
// TODO Auto-generated method stub
HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();
ThreadA aThreadA = new ThreadA(numRef1);
aThreadA.start();
ThreadB bThreadB = new ThreadB(numRef2);
bThreadB.start();
}
}
运行结果:
a set over!
b set over!
b num=200
a num=100
synchronized方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程必须等待比较长时间。在这种情况下可以使用synchronized同步语句块来解决。synchronized方法是对当前对象进行加锁,而synchronized是对某一个对象进行加锁。
2. synchronized(this)代码块
相较于synchronized声明更加灵活,可以只对方法中的部分代码块进行加锁。对于加锁部分同步,而非加锁部分仍然异步。
public class Task {
public void doLongTimeTask() {
//对于非synchronized代码块异步
for(int i = 0; i<10; i++) {
System.out.println("nosynchronized threadName = "
+Thread.currentThread().getName()+" i = "+(i+1));
}
System.out.println();
//对于synchronized代码块同步
synchronized (this) {
for(int j = 0; j<10; j++) {
System.out.println("synchronized threadName = "
+Thread.currentThread().getName()+" j = "+(j+1));
}
}
}
}
public class MyThread1 extends Thread{
private Task task;
public MyThread1(Task task) {
// TODO Auto-generated constructor stub
this.task = task;
}
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
task.doLongTimeTask();
}
}
public class MyThread2 extends Thread {
private Task task;
public MyThread2(Task task) {
// TODO Auto-generated constructor stub
this.task = task;
}
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
task.doLongTimeTask();
}
}
public class Run {
public static void main(String[] args) {
// TODO Auto-generated method stub
Task task = new Task();
MyThread1 thread1 = new MyThread1(task);
thread1.start();
MyThread2 thread2 = new MyThread2(task);
thread2.start();
}
}
运行结果:
nosynchronized threadName = Thread-0 i = 1
nosynchronized threadName = Thread-0 i = 2
nosynchronized threadName = Thread-0 i = 3
nosynchronized threadName = Thread-0 i = 4
nosynchronized threadName = Thread-0 i = 5
nosynchronized threadName = Thread-0 i = 6
nosynchronized threadName = Thread-0 i = 7
nosynchronized threadName = Thread-0 i = 8
nosynchronized threadName = Thread-0 i = 9
nosynchronized threadName = Thread-0 i = 10
synchronized threadName = Thread-0 j = 1
synchronized threadName = Thread-0 j = 2
synchronized threadName = Thread-0 j = 3
synchronized threadName = Thread-0 j = 4
synchronized threadName = Thread-0 j = 5
synchronized threadName = Thread-0 j = 6
synchronized threadName = Thread-0 j = 7
synchronized threadName = Thread-0 j = 8
synchronized threadName = Thread-0 j = 9
synchronized threadName = Thread-0 j = 10
nosynchronized threadName = Thread-1 i = 1
nosynchronized threadName = Thread-1 i = 2
nosynchronized threadName = Thread-1 i = 3
nosynchronized threadName = Thread-1 i = 4
nosynchronized threadName = Thread-1 i = 5
nosynchronized threadName = Thread-1 i = 6
nosynchronized threadName = Thread-1 i = 7
nosynchronized threadName = Thread-1 i = 8
nosynchronized threadName = Thread-1 i = 9
nosynchronized threadName = Thread-1 i = 10
synchronized threadName = Thread-1 j = 1
synchronized threadName = Thread-1 j = 2
synchronized threadName = Thread-1 j = 3
synchronized threadName = Thread-1 j = 4
synchronized threadName = Thread-1 j = 5
synchronized threadName = Thread-1 j = 6
synchronized threadName = Thread-1 j = 7
synchronized threadName = Thread-1 j = 8
synchronized threadName = Thread-1 j = 9
synchronized threadName = Thread-1 j = 10
3. synchronized(非this)代码块
当一个类中有多个synchronized方法,使用synchronized(非this)代码块中的程序与其他同步方法是异步的 ,不与其他锁this同步方法争抢this锁,可以大大提高运行效率。
但是使用synchronized(非this)代码块进行同步操作时,对象监视器必须是同一个对象,而持有不同的对象监视器是异步的。
同一个对象:
public class Service {
private String anyString = new String();
public void doSomething() {
try {
synchronized (anyString) {
System.out.println("线程名称:"+Thread.currentThread().getName()
+" 进入代码块时间:"+System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("线程名称:"+Thread.currentThread().getName()
+" 离开代码块时间:"+System.currentTimeMillis());
}
}catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
不同对象:
public class Service {
public void doSomething() {
try {
String anyString = new String();
synchronized (anyString) {
System.out.println("线程名称:"+Thread.currentThread().getName()
+" 进入代码块时间:"+System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("线程名称:"+Thread.currentThread().getName()
+" 离开代码块时间:"+System.currentTimeMillis());
}
}catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
4. synchronized静态方法
synchronized关键字也可以加在static静态方法上,加在静态方法上是给Class类上锁,而synchronized关键字加在非static静态方法上是给对象上锁。
public class Service {
public synchronized static void printA() {
try {
System.out.println("线程名称:"+Thread.currentThread().getName()
+" 进入printA时间:"+System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("线程名称:"+Thread.currentThread().getName()
+" 离开printA时间:"+System.currentTimeMillis());
}catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
public synchronized static void printB() {
System.out.println("线程名称:"+Thread.currentThread().getName()
+" 进入printB时间:"+System.currentTimeMillis());
System.out.println("线程名称:"+Thread.currentThread().getName()
+" 离开printB时间:"+System.currentTimeMillis());
}
}
5. synchronized(Class)代码块
该方法和synchronized静态方法作用上其实一样,只是用法上略有不同。
public class Service {
public static void printA() {
synchronized (Service.class) {
try {
System.out.println("线程名称:"+Thread.currentThread().getName()
+" 进入printA时间:"+System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("线程名称:"+Thread.currentThread().getName()
+" 离开printA时间:"+System.currentTimeMillis());
}catch (InterruptedException e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
public static void printB() {
synchronized (Service.class) {
System.out.println("线程名称:"+Thread.currentThread().getName()
+" 进入printB时间:"+System.currentTimeMillis());
System.out.println("线程名称:"+Thread.currentThread().getName()
+" 离开printB时间:"+System.currentTimeMillis());
}
}
}