synchronized处理同步问题

JAVA中synchronized关键字用来处理多线程的同步问题,解决每一个线程对象轮番强占资源带来的问题。

它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。

synchronized关键字的使用

使用synchronized关键字处理有两种模式:同步代码块、同步方法。
首先看同步代码块模式:
(1)必须设置一个要锁定的对象,一般可以锁定当前对象:this。

//对静态代码块加synchronized
public class TestTickThread {
    
    public static void main(String[] args) {
        Runnable runnable = new MyTick2Runnable();
        new Thread(runnable, "Thread-黄牛A").start();
        new Thread(runnable, "Thread-黄牛B").start();
        new Thread(runnable, "Thread-黄牛C").start();
        
    }
}


class MyTickRunnable implements Runnable {
    
    private int tick = 10;
    public void run() {
        while (this.tick > 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(this) {
                if (this.tick > 0) {
                    System.out.println(Thread.currentThread().getName() + " 买票, 剩余 " + (--this.tick));
                }
            }
        }
    }
}

(2)使用同步方法:

//对一个类的方法加synchronized
public class testTick2 {
    public static void main(String[] args){
        Runnable runnable=new MyTick2();
        new Thread(runnable,"Thread-黄牛A").start();
        new Thread(runnable,"Thread-黄牛B").start();
        new Thread(runnable,"Thread-黄牛C").start();

    }
}
class MyTick2 implements  Runnable{
    private int tick=10;
    public void run(){
        while(this.tick>0){
            try{
                Thread.sleep(1000);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            this.sale();
        }
    }
    public synchronized void sale(){
        if(this.tick>0){
            System.out.println(Thread.currentThread().getName()+"买票,剩余"+(--this.tick));
        }
    }
}

synchronized关键字的额外说明

对于下面的代码,synchronized用于锁住多个对象,并未锁住它同一对象的同步代码段,所以并未实现线程同步的结果。

public class testSycThread{
    public static void main(String[] args) {
         //通过lock锁锁方法同一对象
         for (int i = 0; i < 3; i++) {
            Thread thread = new MySyncThread1();
            thread.start();
        }
     }
    }

class Sync{
public synchronized   void test(){
        System.out.println("test执行开始,当前线程:"+Thread.currentThread().getName());
        try{
            Thread.sleep(1000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("test执行结束,当前线程"+Thread.currentThread().getName());
    }
  }

 class MySyncThread1 extends Thread{
public void run(){
        Sync sync=new Sync();
        sync.test();
        }
}

synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。即使两个不同的代码段,都要锁同一个对象,那么这两个代码段也不能在多线程环境下同时运行。

对于synchronized实现锁住一个对象的同步代码段,改进方法如下:

(1)synchronized锁住多个线程的同一对象

public class testSycThread{
    public static void main(String[] args) {
         //通过lock锁锁方法同一对象
     Sync sync = new Sync();
        for (int i = 0; i < 3; i++) {
            Thread thread = new MySyncThread2(sync);
            thread.start();
     }
     }
     }

class Sync{
public    void test() {
        synchronized (this) {
            System.out.println("test执行开始,当前线程:" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("test执行结束,当前线程" + Thread.currentThread().getName());
        }
    }
  }

class MySyncThread2 extends Thread {
    private Sync sync ;
    public MySyncThread2(Sync sync) {
        this.sync = sync ;
    }
    public void run() {
        sync.test();
    }
}

(2)synchronized锁这个类对应的Class对象(全局锁):

public class testSycThread{
    public static void main(String[] args) {
         //通过lock锁锁方法同一对象
     Sync sync = new Sync();
        for (int i = 0; i < 3; i++) {
            Thread thread = new MySyncThread2(sync);
            thread.start();
     }
     }
     }

class Sync{
public    void test() {
        synchronized (Sync.class) {
            System.out.println("test执行开始,当前线程:" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("test执行结束,当前线程" + Thread.currentThread().getName());
        }
    }
  }

 class MySyncThread1 extends Thread{
public void run(){
        Sync sync=new Sync();
        sync.test();
        }
}

synchronized优化

Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。

JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。在JDK1.6中,提供了很多的synchronized方法,这里将主要介绍CAS和Java头两种优化方法。

1.CAS优化
CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞。

无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

CAS操作过程如下图:synchronized处理同步问题
CAS并不是武断的将线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。

CAS的问题:
(1)ABA问题

如下图:
synchronized处理同步问题
CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。

解决方案:可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。在JDK1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题。

(2)自旋转问题

转自旋会浪费大量的处理器资源。这是因为当前线程仍处于运行状况,只不过跑的是无用指令。它期望在运行无用指令的过程中,锁能够被释放出来。

解决方案:自适应自旋,即如果自旋转单位时间n内(即旋转的次数)获得到锁,下次自选时间边长些,如果自旋转单位时间n内(即旋转的次数)未获得到锁,下次自选时间边短。

(3)公平性问题

当处于阻塞状态的线程,无法立刻竞争被释放的锁。然而,处于自旋状态的线程,则很有可能优先获得这把锁。
解决方案:使用ock体系可以实现公平锁。

  1. Java对象头

对象的锁可以理解为类似对对象的一个标志,这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。
Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。

四种对象锁的MarkWord变化为下图:
synchronized处理同步问题
对于四种对象锁的升级关系图如下:synchronized处理同步问题
Java虚拟机中synchronized关键字的实现,按照代价由高到低可以分为重量级锁、轻量锁和偏向锁三种:
(1) 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自适应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。
(2) 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
(3)偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。