前言
同一个进程中多条线程的内存是共享的,如果不进行同步处理,会产生不可预知的结果。如何解决同步问题呢? 我们要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等待该线程操作完毕后再进行操作,实现这种方式我们要在线程上加锁,使用关键字synchronized就是其中一种加锁方式,它可以让每个线程依次排队操作共享数据。
原子性,可见性和有序性
什么是原子性,可见性和有序性?
- 原子性:原子是构成物质的基本单位,所以原子的意思是------"不可分",原子性的操作拒绝线程调度器中断。
- 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到,称为可见性。
- 有序性:有序性是指程序按照代码的先后顺序执行。编译器为了优化性能,有时会改变程序中语句的执行顺序,但是不会影响最终的结果。有序性的经典案例就是利用DCL双重检查创建单例对象。
synchronied可以保证原子性,可见性和有序性。
synchronized的应用方式
synchronized有以下3种应用方式:
- 用在普通方法上,加锁的对象是当前实例,进入方法前需要获取当前实例的锁;
- 用在静态方法上,加锁的对象是当前类对象,进入方法前需要获取当前类对象的锁;
- 用来修饰代码块,这种方式需要自己指定加锁对象,进入代码块前要获得指定对象的锁;
synchronized作用于实例方法
看如下代码:
java
public class AccountingSync implements Runnable{
// 共享资源(临界资源)
static int i=0;
// synchronized修饰普通方法
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join(); // join:当前线程要等待调用join()的线程执行完毕才能继续往下执行
t2.join();
System.out.println(i);
}
}
在上述代码中,我们开启两个线程操作同一个共享资源------变量i,由于i++操作不具备原子性,该操作是先读取旧值,然后在旧值的基础上加1,最后写回新值,总共分3步完成。如果第2个线程在第1个线程读取旧值和写回新值期间读取i的值,那么第2个线程与第1个线程读取的是就是同一个值,并执行相同值的加1操作,最后写回的也就是同一个值。这样就造成了线程安全问题,显然不是我们想要的结果。
为了解决这个问题,我们使用synchronized修饰普通方法increase(),在种情况下,当前锁的对象便是instance实例。运行代码,最终输出结果是2000000,从执行结果来看确实是正确的,倘若没有使用synchronized关键字,其最终输出结果会小于2000000,这便是synchronized关键字的作用。
但这种情况下如果给2个线程传入不同的实例,线程安全就无法保证了,看如下代码:
java
public class AccountingSyncBad implements Runnable{
static int i=0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
// 传入新实例
Thread t1=new Thread(new AccountingSyncBad());
// 传入新实例
Thread t2=new Thread(new AccountingSyncBad());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
上述方法我们使用synchronized修饰了increase()方法,但是new了两个不同的实例,这也就意味着锁的对象是2个不同的实例,也就是说t1和t2线程中锁的对象不同,因此线程安全是无法保证的。
解决这个问题的方法是将increase()方法改成静态方法,这样锁的对象就是当前类对象,无论创建多少个实例对象,类对象只有一个,这样2个线程锁的对象是相同的。下面我们看看如何将synchronized作用于静态方法。
synchronized作用于静态方法
当synchronized作用于静态方法时,锁的对象是当前类对象。静态方法是类成员,比如静态的increase()方法可以直接通过AccountingSyncClass.increse()调用,不管你new多少个实例,类对象只有一个。因此把上面的increase()方法改成static修饰,就可以拿到正确的打印结果。代码如下:
java
public class AccountingSyncBad implements Runnable{
static int i=0;
// 静态synchronized方法,锁的是当前类对象
public static synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
// 传入新实例
Thread t1=new Thread(new AccountingSyncBad());
// 传入新实例
Thread t2=new Thread(new AccountingSyncBad());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
需要注意的是如果线程A调用实例的非静态synchronized方法,而线程B调用新实例的静态synchronized方法,这种不会发生互斥现象,因为两个线程锁的对象不一样,看如下代码:
java
public class AccountingSync implements Runnable{
private boolean flag;
public AccountingSync(boolean flag){
this.flag = flag;
}
static int i=0;
// 静态synchronized方法,锁的对象是当前类对象
public static synchronized void increase(){
i++;
}
// 非静态synchronized方法,锁的对象是当前实例
public synchronized void increase4Obj(){
i++;
}
@Override
public void run() {
if(flag) {
for(int j=0;j<1000000;j++) {
increase();
}
}else {
for(int j=0;j<1000000;j++) {
increase4Obj();
}
}
}
public static void main(String[] args) throws InterruptedException {
// new新实例
Thread t1=new Thread(new AccountingSync(true));
// new新实例
Thread t2=new Thread(new AccountingSync(false));
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
其打印结果会小于2000000。
synchronized作用于同步代码块
除了可以使用synchronized关键字修饰实例方法和静态方法外,还可以用来修饰同步代码块,在某些情况下,我们编写的方法体可能比较大,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,同步代码块的使用示例如下:
java
public class AccountingSync implements Runnable{
private Object object = new Object();
static int i;
static int m;
@Override
public void run() {
for(int k=0; k < 1000000; k++){
m++;
}
synchronized(object){
for(int j=0; j < 1000000; j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(m);
System.out.println(i);
}
}
上述代码运行后,m的打印值小于2000000,而i的打印值是2000000。
这里synchronized锁的object对象没有用static修饰,锁的对象是当前实例。如果把object改成static修饰,锁的对象就是类对象了,这种情况与synchronized作用于静态方法类似。由此可知,synchronized作用于同步代码块,锁的对象与synchronized修饰的对象有关。
另外,使用sychronied修饰普通方法,其实也等价于synchronized修饰同步代码块并包裹整个方法的synchronized(this){},意思是下面2段代码是等价的:
java
public synchronized void increase(){
i++;
}
java
public void increase(){
synchronized(this){
i++;
}
}
使用sychronied修饰静态方法,其实也等价于synchronized作用于同步代码块并包裹整个方法的synchronized(class){},下面2段代码也是等价的:
java
public static synchronized void increase(){
i++;
}
java
public void increase(){
synchronized(AccountingSync.class){
i++;
}
}