synchronized 处理同步问题

所谓的同步指的是所有的线程不是一起进入到方法中执行,而是按照顺序一个一个进来

一、synchronized同步处理
  1. 使用同步代码块 : 如果要使用同步代码块必须设置一个要锁定的对象,所以一般可以锁定当前对象this,表示同一时刻只有一个线程能够进入同步代码块,但是多个线程可以同时进入方法,比如for循环。
    同步虽然可以保证数据的完整性(线程安全操作),但是其执行的速度会很慢
  2. 使用同步方法:在方法上加synchronized,表示此时只有一个线程进入同步方法
注意ifwhile的区别,
if的话可以每个线程都会卖票
while的话只有一个卖完,其他的才会卖
但是有时候ifwhile结果一样,比如锁方法的时候
public class TestThread5 {
    public static void main(String[] args) {

        //共享业务逻辑
        //或者写成Runnable runnable = new TicketRunnable();
        TicketRunnable runnable = new TicketRunnable();
        Thread threadA = new Thread(runnable, "黄牛A");
        Thread threadB = new Thread(runnable, "黄牛B");
        Thread threadC = new Thread(runnable, "黄牛C");

        threadA.setPriority(Thread.MAX_PRIORITY);
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

class TicketRunnable implements Runnable {

    private int ticket = 10;
//会出现负的,所以引入同步处理
//    @Override
//    public void run() {
//
//        while (this.ticket > 0) {
//            try {
//                Thread.sleep(1000);
//
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            System.out.println(Thread.currentThread().getName() + "还有" + this.ticket-- + "张票");
//        }
//    }
//}
----------------------------------------------------------------------
1.锁对象
//1.有for循环
//    @Override
//    //1.代码局部用同步块
//     //多个线程可以同时进入run方法,for循环
    public void run() {
         //同一时刻,只允许一个线程进入代码块处理
        System.out.println(Thread.currentThread().getName()+","+Thread.currentThread().getPriority());
        for (int i = 0; i < 10; i++) {
        //多个线程进入for循环,一共会输出30个----,for中每次进入3个线程
            System.out.println("--------");
          //表示为程序逻辑上锁
          //只有一个线程进入以下执行,由于是while循环,所以一个线程一直卖完后
          //其他线程进入,由于共享,其他线程是能输出票卖完了。
          //如果是if语句,可以轮着卖,一个执行完执行另一个
            synchronized (this) {//this->Runnable对象,部分锁定
                while (this.ticket > 0) {
                //一直执行循环
                    try {
                        Thread.sleep(100);
                        System.out.println(Thread.currentThread().getName()
                                + "还有" + (this.ticket--) + "张票");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        System.out.println(Thread.currentThread().getName() + "卖完了");
    }
----while执行的结果------
黄牛A,10
--------
黄牛C,5
--------
黄牛B,5
--------
黄牛A还有10张票
黄牛A还有9张票
黄牛A还有8张票
黄牛A还有7张票
黄牛A还有6张票
黄牛A还有5张票
黄牛A还有4张票
黄牛A还有3张票
黄牛A还有2张票
黄牛A还有1张票
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
黄牛B卖完了
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
黄牛C卖完了
黄牛A卖完了
--------------------------------if执行结果-------------
黄牛A,10
黄牛C,5
--------
黄牛B,5
--------
--------
黄牛C还有10张票
--------
黄牛A还有9张票
--------
黄牛B还有8张票
--------
黄牛A还有7张票
--------
黄牛C还有6张票
--------
黄牛A还有5张票
--------
黄牛B还有4张票
--------
黄牛A还有3张票
--------
黄牛C还有2张票
--------
黄牛A还有1张票
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
--------
黄牛A卖完了
黄牛C卖完了
黄牛B卖完了
//2,没有for循环的,和上面的差不多
  @Override
    public void run() {
        //2. 加了锁只能有一个对象进来,全部锁定
        //该方法同一时刻只会让一个黄牛卖票
        synchronized (this) {//this->Runnable对象
            while (ticket > 0) {
                    try {
                        Thread.sleep(100);
                        System.out.println(Thread.currentThread().getName()
                                + "还有" + (this.ticket--) + "张票");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
            }
                System.out.println(Thread.currentThread().getName() + "卖完了");
      }
 }
 -----while执行的结果--------
黄牛A还有10张票
黄牛A还有9张票
黄牛A还有8张票
黄牛A还有7张票
黄牛A还有6张票
黄牛A还有5张票
黄牛A还有4张票
黄牛A还有3张票
黄牛A还有2张票
黄牛A还有1张票
黄牛A卖完了
黄牛B卖完了
黄牛C卖完了
---if执行的结果-------
没有for循环所以每个执行一次
黄牛A还有10张票
黄牛A卖完了
黄牛C还有9张票
黄牛C卖完了
黄牛B还有8张票
黄牛B卖完了
---------------------------------------------------------------------
2.锁方法,本质是实例对象  
    @Override
    //3.方法锁定,全部锁定,和上面那个结果相同
    public synchronized void run() {
        for (int i = 0; i < 10; i++) {
        //只有一个线程进入,由于共享,所以一个进去之后票会卖完
            if (this.ticket > 0) {
           // while和if是一样的结果
                try {
                    Thread.sleep(200);
                    System.out.println(Thread.currentThread().getName()
                            + "还有" + (this.ticket--) + "张票");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(Thread.currentThread().getName() + "卖完了");
    }
黄牛A还有10张票
黄牛A还有9张票
黄牛A还有8张票
黄牛A还有7张票
黄牛A还有6张票
黄牛A还有5张票
黄牛A还有4张票
黄牛A还有3张票
黄牛A还有2张票
黄牛A还有1张票
黄牛A卖完了
黄牛C卖完了
黄牛B卖完了

-----------------------------------------------------------------------    
3.  相当于全局锁
     //同步方法,进入到方法中的线程可能为多个
    // if:6次执行完后在执行下一个黄牛
    //while:由一个线程全部卖完,比如1000张票,全部都是A在执行
    @Override
    public void run() {
    //一次只能一个对象进去执行,执行6次后释放锁,相当于进入for循环的虽然有多个线程
    //但是执行sale方法的每次只有一个this
        for (int i = 0; i < 6; i++) {
            this.sale();
        }
    }
        public synchronized void sale(){
        //如果是while的话,A全部卖完
            if(this.ticket>0){
                try{
                    Thread.sleep(200);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ",还有" + this.ticket--
                        + " 张票");
            }
        }
    }
黄牛A,还有1000 张票
黄牛A,还有999 张票
黄牛A,还有998 张票
黄牛A,还有997 张票
黄牛A,还有996 张票
黄牛A,还有995 张票
黄牛B,还有994 张票
黄牛B,还有993 张票
黄牛B,还有992 张票
黄牛B,还有991 张票
黄牛B,还有990 张票
黄牛B,还有989 张票
黄牛C,还有988 张票
黄牛C,还有987 张票
黄牛C,还有986 张票
黄牛C,还有985 张票
黄牛C,还有984 张票
黄牛C,还有983 张票
二、Synchronized对象锁概念

1.锁多对象。对象锁:各锁各的对象,同时进,同时出

//(1)锁不住,各锁各的对象
public class TestSynchronized3 {
    public static void main(String[] args) {
        MyThread2 myThread = new MyThread2();
        for (int i = 0; i < 3; i++) {
            new Thread(myThread, "线程+i").start();
        }
    }
}

class MyThread2 implements Runnable {
    @Override
    public void run() {
        //各锁各的对象,互不影响,同时进入3个坑
        //sync1--线程0
        //sync2--线程1
        Sync2 sync = new Sync2();
        sync.test();
    }

}

class Sync2 {
    public void test() {
    //锁的是对象,但是每次new的对象都不同,
        synchronized(this){
            System.out.println("begin");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("over");
        }

    }
}
 begin
 begin
 begin
 over
 over
 over
 (2)改成以下这样,只有一个对象,能锁住,只修改了Test1方法
 class Sync {
    public void tt() {
        synchronized (this) {
            System.out.println("begin:"+Thread.currentThread().getName());
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("over:"+Thread.currentThread().getName());
        }
    }
}

class Test1 extends Thread {
    private Sync sync;

    public Test1( Sync sync) {
        this.sync = sync;
    }

   @Override
    public void run(){
        this.sync.tt();
    }
}

public class TestSynchronized {
    public static void main(String[] args) {
        Sync sync=new Sync();
        for(int i=0;i<3;i++){
            Thread test1 = new Test1(sync);
            test1.start();
        }
    }
}
 * begin:Thread-2
 * over:Thread-2
 * begin:Thread-1
 * over:Thread-1
 * begin:Thread-0
 * over:Thread-0

实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的同步代码段。即synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。

即使两个不同的代码段,都要锁同一个对象,那么这两个代码段也不能在多线程环境下同时运行。如果想要锁住这个代码块,我们可以对于这种情况我们可以锁对象、锁类的Class对象
2.全局锁:锁代码段
(1)使用类的静态同步方法
static 和synchronized一起使用,public static synchronized void test1()锁住多个对象的同一方法,使用的是该全局锁,锁的是类不是this,方法也相当于全局锁
(2)在代码段中锁Class对象synchronized (Sync1.class)

//第一个:这个就和上面的3很像了,一次一个线程执行
//俩个方式都是一次一个线程执行
class Sync1 {
    public static synchronized void test1() {
       // synchronized (Sync1.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 TestSync extends Thread {

    @Override
    public void run() {
        Sync1 sync1 = new Sync1();
        sync1.test1();
    }
}

public class TestSynchronized2 {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new TestSync().start();
        }
    }

}
test方法开始,当前线程为 Thread-2
test方法结束,当前线程为 Thread-2
test方法开始,当前线程为 Thread-0
test方法结束,当前线程为 Thread-0
test方法开始,当前线程为 Thread-1
test方法结束,当前线程为 Thread-1

Synchronized用的位置可以有这些使用场景:
分为两类:使用在方法和代码块中,被锁的对象有类对象和实例对象,1和3、2和4
在这里插入图片描述

三、Synchronized底层实现

monitor对象锁机制是JDK1.6之前synchronized底层原理,又称JDK1.6重量级锁
同步代码底层实现
执行同步代码块后首先要先执行monitorenter指令,获得锁,退出的时候执行monitorexit指令,进行解锁。通过分析之后可以看出,使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行同步代码段,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。
通常一个monitorenter指令会同时包含多个monitorexit指令,因为JVM确保所获得到的锁在正常执行路径或者是异常执行路径下都可以正确解锁。就算程序崩了,你也得先释放锁。

同步静态方法底层实现

当使用synchronized标记方法时,字节码会出现访问标记ACC_SYNCHRONIZED。该标记表示在进入该方法时,JVM需进行monitorenter 操作,在退出该方法时,不管是正常返回,还是向调用者抛异常,Java 虚拟机均需要进行monitorexit操作。这里 monitorenter 和 monitorexit 操作所对应的锁对象是隐式的。对于实例方法来说,这两个操作对应的锁对象是 this;对于静态方法来说,这两个操作对应的锁对象则是所在类的 Class实例
1. 计数器
关于 monitorenter 和 monitorexit 的作用,我们可以抽象地理解为每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。目的是为了同一个线程可以重复获取同一把锁

  • 首先当执行 monitorenter 时,如果目标锁对象的计数器为 0,那么说明它没有被其他线程所持有。在这个情况下,Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加 1。
  • 其次在目标锁对象的计数器不为 0 的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1(可重入锁),可以持续加,否则需要等待,直至持有线程释放该锁。
  • 最后当执行 monitorexit 时,Java 虚拟机则需将锁对象的计数器减 1。当计数器减为 0 时,那便代表该锁已经被释放掉了,唤醒所有等待的线程去竞争该锁。
public class TestLock {
    public static void main(String[] args) {
        LockRunnable t = new LockRunnable();
        Thread thread1 = new Thread(t, "A");
        Thread thread2 = new Thread(t, "B");
        thread1.start();
        thread2.start();
    }
}

class LockRunnable implements Runnable {
    private int ticket = 10;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("----");
            lock.lock();//加锁
            try {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + "还有" + --ticket + "张票");
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();//一定解锁成功
            }
        }
    }
}
四、Synchronized优化

Synchronized内建锁它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性),这种方式肯定效率低下,每次只能通过一个线程,阻塞同步。
而CAS不是将线程挂起,当CAS失败后会进行一定的尝试操作,而不是挂起线程,非阻塞同步

未优化锁互斥同步(阻塞同步),CAS非阻塞同步

1. CAS(无锁操作) :JKDK1.5后出现的

  • 悲观锁:线程获取锁(JDK1.6之前内建锁),是一种悲观锁策略,假设每一次执行临界区代码时都会发生冲突,(每次都有人和我抢),所以当前线程获取到锁的时候同时也会阻塞其他未获取到锁的线程。
  • 乐观锁:所有线程访问共享资源的时候不会出现冲突,(一定没人和我抢)不会出现冲突就不会阻塞其他线程的操作,因此线程就不会出现阻塞停顿的状态。若有冲突的话:无锁操作使用CAS来鉴别线程是否发生冲突,若发生就重试当前操作直到没有冲突为止。
  • CAS是这样交换的,包含三个值实际值、期望值(旧值)、更新值,V、O、N,当V和O一致时,说明没有被其他线程修改,O是最新值,N赋给V,否则,说明被其他线程修改,O不是最新值,不能将新值N赋给V,返回V
  • 当多个线程使用CAS(compare and swap)操作一个变量时,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试(自旋),当然也可以选择挂起线程(阻塞)。
  • Java虚拟机中synchronized关键字的实现,按照代价由低到高可以分为无锁----->偏向锁----->轻量锁----->重量级锁四种,锁只能升级却不能降级,目的
    是为了提高获得锁和释放锁的效率。
    出现的问题(1)ABA问题:加版本号(2)自旋会浪费大量的处理器资源
    与线程阻塞相比,自旋会浪费大量的处理器资源。这是因为当前线程仍处于运行状况,只不过跑的是无用指令。它期望在运行无用指令的过程中,锁能够被释放出来。解决方法:自适应自旋,根据以往自旋等待时能否获取锁,来动态调整自旋的时间(循环数)。如果在自旋的时候获取到锁,则会延长下一次自旋时间;否则就稍微减少下一次自旋时间(3)公平性:内建锁无法解决,Lock锁可以解决

2. 对象头

  • 对对象的一个标志,这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode、分代年龄和锁标记位。无锁:0 01;偏向锁:1 01;轻量级锁:00 ;重量级锁:10
  • 竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

3. 三个锁
1.偏向锁:JVM虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况,是锁状态中最乐观的一种锁,从始至终只有一个线程请求某一把锁。
(1)偏向锁的获取

  • 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,直接进入,自旋没有这个过程
  • 当一个线程访问同步块失败时,使用CAS竞争锁,尝试使用CAS将对象头的偏向锁指向当前线程,并升级为轻量级锁。
    (2)偏向锁的撤销:开销大
    偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,并将锁膨胀为轻量级锁(持有偏向锁的线程任然存活)。如果持有线程已经终止,将锁对象的对象头设置为无锁状态。
    (3)关闭偏向锁
    JDK1.6默认启动,如果应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

2.轻量级锁:采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况,来避免线程的阻塞以及唤醒。
(1)加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞锁,当前线程便尝试使用自旋来获取锁。
(2)解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
3. 重量级锁:JVM虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。它针对锁仅会被同一线程持有的情况,同一时间多个线程竞争锁。JVM采用了自适应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。
4.锁粗化
就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁
5. 锁消除
锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。
6. 死锁
同步的本质在于:一个线程等待另外一个线程执行完毕执行完成后才可以继续执行。但是如果现在相关的几个线程彼此之间都在等待着,那么就会造成死锁,过多的同步会造成死锁。

三种锁的特点

  • 偏向锁只会在第一次请求锁时采用CAS操作并将锁对象的标记字段记录下当前线程地址。在此后的过程中。持有偏向锁的线程无须加锁操作。
    针对的是锁仅会被同一线程持有的状况
  • 轻量级锁采用CAS操作,将锁对象标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象本来的标记字段。
    针对的是多个线程在不同时间段申请同一把锁的情况
  • 重量级锁会阻塞、唤醒请求加锁的线程。
    针对的是多个线程同时竞争同一把锁的情况,JVM采用自适应自旋,来避免在面对非常小的同步代码块时,仍会被阻塞和唤醒的状况。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值