javaSE高级开发多线程——4 线程的同步与死锁 (这一篇内容只需要掌握概念就可以)

1.同步处理
同步处理:指的是同步所有线程,保证同一时间的线程不是一起进入到方法或者代码块,必须得一个一个来。

package com.wschase.thread2;

/**
 * Author:WSChase
 * Created:2019/1/9
 */
class MyThread implements Runnable {
    private int ticket = 10 ; // 一共十张票
     @Override    public void run() {

         while (this.ticket > 0) { // 还有票
              try {

              Thread.sleep(200);

                } catch (InterruptedException e) {

              //TODO Auto-generated catch block
               e.printStackTrace();
              } // 模拟网络延迟
              System.out.println(Thread.currentThread().getName()+",还有" +this.ticket -- +" 张票");
         }
     }
}


public class MyRunnable {
    public static void main(String[] args) {
        MyThread mt = new MyThread() ;
        new Thread(mt,"黄牛A").start();
        new Thread(mt,"黄牛B").start();
        new Thread(mt,"黄牛C").start();
         }
     }

在这里插入图片描述
从上面的例子我们可以看出来,这是一个线程不同的例子,它会同时出现多个用户抢票。
(1)synchronized处理同步问题
如果想要实现这把锁的功能,那么就可以采用synconized关键字。它的处理有两种模式:同步代码块、同步方法。
使用同步代码块方式:如果想对代码的局部做同步,就用我们的局部块。

package com.wschase.thread2;

/**
 * Author:WSChase
 * Created:2019/1/9
 */
class MyThread implements Runnable {
    private int ticket = 10; // 一共十张票

    @Override
    public void run() {

        for (int i = 0; i < 10; i++) {
            //在同一时间,只允许一个线程进入代码块
            System.out.println("----");//如果没有这一句话,一旦有一个进入到循环里面占用了资源那么就会一直在里面
                                                   //但是加上这一句话以后就可以同时有多个线程进入到这个循环里面,所以运行的结果                   
                                                   //才是有A,有B,有C。
            synchronized (this) {
                if (this.ticket > 0) {
                    try {

                        Thread.sleep(200);

                    } catch (InterruptedException e) {

                        //TODO Auto-generated catch block
                        e.printStackTrace();
                    } // 模拟网络延迟
                    System.out.println(Thread.currentThread().getName() + ",还有" + this.ticket-- + " 张票");
                }
            }
        }
    }
}


public class MyRunnable {
    public static void main(String[] args) {
        MyThread mt = new MyThread() ;
        new Thread(mt,"黄牛A").start();
        new Thread(mt,"黄牛B").start();
        new Thread(mt,"黄牛C").start();
         }
     }

在这里插入图片描述

使用局部方法方式:如果我们想把一个方法里面所有的逻辑给它做同步,那么我们就使用同步方法。

package com.wschase.thread2;

/**
 * Author:WSChase
 * Created:2019/1/9
 */
class MyThread implements Runnable {
    private int ticket = 10; // 一共十张票

    @Override
    public void run() {

        for (int i = 0; i < 10; i++) {
            System.out.println("---");
            this.sale();
        }
    }
     public synchronized void sale() {
         if (this.ticket > 0) {
             try {

                 Thread.sleep(200);

             } catch (InterruptedException e) {

                 //TODO Auto-generated catch block
                 e.printStackTrace();
             } // 模拟网络延迟
             System.out.println(Thread.currentThread().getName() + ",还有" + this.ticket-- + " 张票");
         }
     }
}


public class MyRunnable {
    public static void main(String[] args) {
        MyThread mt = new MyThread() ;
        new Thread(mt,"黄牛A").start();
        new Thread(mt,"黄牛B").start();
        new Thread(mt,"黄牛C").start();
         }
     }

在这里插入图片描述
对于以上两种方法就是线程同步处理的两种方法,从上面的方法我们可以看到,在循环里面的线程同步处理,我们一定要注意,如果循环里的第一句话就用了synchnized修饰了的话,那么只要有一个线程进入循环,将会一直占用资源直到循环停止。
2.关于synchronized的额外说明
实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行一个对象的同步代码块。即synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁住的是对象本身也就是this。
3.锁住代码块
全局锁:synchronized(class)
这个的意思是锁住这个类,只要是属于这个类的对象都会被锁住。全局锁是有风险的。
对象锁:synchronzied(this/obj)
对象锁锁住的只是这个对象,它的范围更小一些。
4.synchronized是怎么实现的
在这里插入图片描述
执行同步代码块以后首先需要执行的指令是:monitorenter,退出的时候执行指令:monitorexit。通过分析以后我们可以看到,使用synchornized进行同步的关键在于必须要对对象的监视器monitor进行获取,当线程获取但监视器monitor以后才可以继续往下执行代码,否则之恩等待。而这个获取的过程是相互排斥的,即同一时间只能有一个线程获取监视器monitor。
5.显示锁

package com.wschase.suo;

import java.time.LocalDateTime;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**线程的同步与死锁
 * Author:WSChase
 * Created:2019/1/10
 */
public class Testsuo {
    //对于同步与死锁我们只需要掌握概念就可以了
    public static void main(String[] args) {
        LockRunnable runnable=new LockRunnable();

        Thread thread1=new Thread(runnable,"黄牛A");
        Thread thread2=new Thread(runnable,"黄牛B");
        Thread thread3=new Thread(runnable,"黄牛C");
        thread1.start();
        thread2.start();
        thread3.start();


    }
}

class LockRunnable implements Runnable {

    private int tick = 10;

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        //我们把加锁放在try之前,把解锁放在fainally里面,这样可以保证我们的锁一定可以加锁和解锁成功
        //这样才可以达到我们想要的效果:就是一个线程不会长期的占用一把锁
        for (int i = 0; i < 10; i++) {
            System.out.println("-----");//不要让循环和锁紧挨着
            lock.lock();//加锁
            try {

                if (this.tick > 0) {
                    System.out.println(Thread.currentThread().getName() + "剩余票数" + --this.tick);

                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //多线程没有办法同时访问这块代码
                lock.unlock();//解锁

            }
        }

    }
}

通过Lock,显示加锁,我们可以先加在哪就在哪。
在这里插入图片描述
如果我们把sleep()放在循环紧跟的第一句话,那么我们就不需要打印-----了,这样也可以保证出来的结果A、B、C都会出现。
注意:虽然Lock可以自由的使用,显示的使用,但是我们的synchronized是隐式的但是经过不断的优化以后,我们的synchronized已经变得更好了,所以我们还是优先使用synchronized关键字实现线程同步加锁。
对于synchronized我们掌握下面这一张表就可以啦:
在这里插入图片描述
6.synchronized优化
(1)CAS(竞争锁)
我们使用CAS也是来给线程进行加锁和解锁的。使用锁的时候,线程获取锁是一种悲观策略,悲观策略:当前线程进去以后其他线程就不可以进去了,这样其他线程就会产生阻塞了。而CAS操作(又称为无锁操作)也是一种乐观锁策略,它就阿佘所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然就不会阻塞其他线程的操作了。那么,如果出现了冲突该怎么办?无锁操作时使用CAS compare and swap(比较转换)来鉴别线程是否出现冲突的,如果出现了冲突就重试当前操作直到没有冲突为止。
7.锁的几种状态
记住
锁一共有4种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(他们的优先级是逐渐升级的,还有锁是不能降级的,它的目的是为了提高获得锁和释放锁的效率)。
(1)偏向锁
这是最乐观的一种锁:从始至终只有一个线程请求一把锁。
偏向锁的获取:分为两个过程
首先判断当前线程是不是偏向锁,如果测试成功了,那么它已经获得了偏向锁了;如果不是,我们再来看这个对象是否有标志位表示偏向锁的标志位,然后再进行测试,这时候如果测试出来的标志位为1表示当前锁是偏向锁,如果测试出来是0,则使用CAS竞争锁;如果已经设置了竞争锁,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销:
等到竞争出现才释放锁的机制;所以当其他线程竞争偏向锁的时候,持有偏向锁的线程才会释放锁。
(2)轻量锁
多线程在不同时段请求一把锁,也就是说没有锁竞争。只有在同一个之间段多线程访问才存在竞争。
加锁:在每次获取的时候都需要先创建栈帧,把对象头拷贝到空间里面来,然后我再用CAS去做竞争,如果成功了我就获取到当前的锁了,如果失败了,说明有人和我在竞争,我就额人家竞争。
解锁:
使用原子的CAS操作将轻量锁替换回对象头(Mark world),但是替换不一定能成功,因为有可能会有其他线程在使用这个对象头,所以如果替换成功了则表示没有竞争,如果不成功,则表明有竞争,有竞争以后还得想办法释放,这时候就将锁编程重量级的锁——锁膨胀,这就回到synchronized。升级完重量级锁以后,一旦我们的锁已释放,就会进行新一轮的竞争。
synchronized(重量级锁的特点):多线程在同一时间竞争一个资源,谁竞争成功以后就会造成其他线程阻塞。
(3)重量级锁
它是JVM种最为基础的锁实现 。在重量级锁中,JVM会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。
java线程中的阻塞和唤醒都依赖于我们的操作系统。
为了避免这样的阻塞以及唤醒状态,我们有一个自旋状态,通过自旋来减轻阻塞以及唤醒的状态,减轻操作系统不停的转换。
总结:(对锁的掌握就这三句话)
(1)重量级锁会引起阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自旋,来避免线程在面对非常小的synchronized代码块,仍会被阻塞、唤醒的情况。
(2)轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
(3)偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的时锁仅会被同一线程持有的情况。
8.锁的其他优化
(1)锁粗化
锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁。
(2)锁清楚
当我们在一个方法里面执行一段代码的时候,只要这段代码一直在这段代码的作用域里面,我们就可以不需要考虑使用线程安全。因为它始终时属于这个代码块的,多线程是访问不到这个代码块的。
9.死锁
同步的本质在于:一个线程得等到另一个线程执行完毕以后擦可以执行,并且如果现在相关的几个线程彼此之间都在等待,那么就会造成死锁。
死锁的例子:相互去竞争资源

package com.wschase.suo;

/**死锁
 * Author:WSChase
 * Created:2019/1/10
 */
public class TestDeadLock {

    //资源笔
    private static Pen pen=new Pen();

    //资源书
    private static Book book=new Book();
    public static void main(String[] args) {

        //线程1
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (book){
                    System.out.println(Thread.currentThread().getName()+"我有书,但是还在用");

                    synchronized (pen){
                        System.out.println(Thread.currentThread().getName()+"想做笔记,但是没有笔,把你的给我");
                    }
                }
            }
        },"Thread-A");



        //线程2
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (pen){
                    System.out.println(Thread.currentThread().getName()+"我有笔,但是还在用");

                    synchronized (book){
                        System.out.println(Thread.currentThread().getName()+"想看书,但是没有书,把你的给我");
                    }
                }
            }
        },"Thread-B");

        thread1.start();
        thread2.start();

    }
}

class Pen{
    private String name="笔";

    public String getName() {
        return name;
    }
}

class Book{
    private String name="书";

    public String getName() {
        return name;
    }
}

10.ThreadLocal
(1)概念
ThreadLocal用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其他线程。它于同步机制正好是相反了,同步时保证多线程访问数据的一致性。而ThreadLocal保证的时多线程变量的独立性。

package com.wschase.suo;

/**
 * Author:WSChase
 * Created:2019/1/10
 */
public class TestThreadLocal {

    //多线程共享
    private static String staticCommValue;
    
    //多线程独立
    private static ThreadLocal<String> threadLocal=new ThreadLocal<>();

    public static void main(String[] args) {
        //main方法是在Thread - main 线程中执行
        //1.主线程中修改staticCommValue
        staticCommValue = "这是main线程修改的值";
        //2.在主线程中修改threadLocal
        threadLocal.set("这是main线程修改的threadLocal的值");

        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                //3.子线程中修改staticCommValue
                staticCommValue = "这是子线程修改的值";
                //4.在子线程中修改threadLocal
                threadLocal.set("这是子线程修改的threadLocal的值");

                System.out.println("子线程"+threadLocal.get());
            }
        },"子线程");

        thread.start();

        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //打印输出
        System.out.println(staticCommValue);
        System.out.println(threadLocal.get());
    }
}

在这里插入图片描述
这个时候我们的资源是共享的。

猜你喜欢

转载自blog.csdn.net/ZhuiZhuDream5/article/details/86173311