java多线程学习总结(二)

版权声明:支持原创,注明出处。 https://blog.csdn.net/qq_38584967/article/details/87933986

前言

上一篇概述了一些线程的状态和方法,下面介绍一下线程安全。

线程安全

线程工作都有单独的工作空间,一般都是先拷值过来修改再更新值,存在更新值之前其他线程也进行了值拷贝,导致值存在不同步的问题,线程不安全。

线程并发三要素:
	同一个对象
	多个线程
	同时操作

存在线程安全问题,于是就需要实现线程同步,线程同步的步骤有:形成队列、等待队列、加上锁(锁机制synchronized)。线程同步的任务是保证安全和性能。

synchronized

使用synchronized锁住资源,每次只允许一个线程对锁住的资源进行访问修改操作,保证资源的同步。
使用synchronized分为同步方法和同步块,用synchronized修饰的范围太大将会大大影响效率,比如一个方法内有A和B两个属性,A只读,B需同步写,如果用同步方法,则会影响读A的效率,用同步块来缩小同步范围。
线程不安全实例

package threadtest.syn;

import mainTest.Test;
import study_1.Main;

/**
 * 购票不同步导致的问题
 */
public class UnsafeTest_01 {
    public static void main(String[] args) {
        UnsafeWeb12306 web12306 = new UnsafeWeb12306();
        Thread t1 = new Thread(web12306,"农民");
        Thread t2 = new Thread(web12306, "白领");
        Thread t3 = new Thread(web12306, "中产");
        Thread t4 = new Thread(web12306, "资产");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

class UnsafeWeb12306 implements Runnable{

    private int ticketNums = 10;
    private boolean flag = true;
    @Override
    public void run() {
        while (flag){
            test();
        }

    }
    public void test(){
        if (ticketNums < 1){
            setFlag(false);
        }else {
            //可能产生的问题
            //1.当ticketNums=1线程进来时,休眠一会,这个时候还没有对值进行修改,
            // 而这时有新的线程也通过ticketNums=1进来,
            // 这个时候就会导致ticketNums被两个线程先后修改,出现ticketNums=0甚至负数的情况
            //2.线程工作有单独的工作空间,先拷值过来修改再更新值,存在更新值之前其他线程也进行了值拷贝,
            // 两个线程拷贝了相同的值,导致修改后打印的值相同,线程不安全。
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() 
            	+ "-->" + ticketNums--);
        }
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

线程不安全,出现了打印出票号小于0或者相同票号的情况
在这里插入图片描述
分别使用同步方法和同步块来解决线程安全问题

package threadtest.syn;

public class SyncTest_01 {
    public static void main(String[] args) {
        SafeWeb12306 web12306 = new SafeWeb12306();
        Thread t1 = new Thread(web12306,"农民");
        Thread t2 = new Thread(web12306, "白领");
        Thread t3 = new Thread(web12306, "中产");
        Thread t4 = new Thread(web12306, "资产");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

class SafeWeb12306 implements Runnable{
    //设置票数大一些,否则看不到效果
    private int ticketNums = 100;
    private boolean flag = true;
    @Override
    public void run() {
        while (flag){
//            test();
            test2();
        }

    }

    /**
     * 同步方法实现线程同步
     */
    public synchronized void test(){
        if (ticketNums < 1){
            setFlag(false);
        }else {
            try {
                //模拟延时
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + "-->" + ticketNums--);
        }
    }

    /**
     * 同步块实现线程同步,需要控制ticketNums和flag,
     * 所以同步块可以锁定他们所在的类对象即this,不可只锁其中一个变量。
     */
    public void test2(){
        synchronized(this) {
            if (ticketNums < 1) {
                setFlag(false);
            } else {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + "-->" + ticketNums--);
            }
        }
    }


    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

synchronized锁的是资源,所以需要看清你需要锁的资源是哪一个,然后再对资源进行锁定。
synchronized(obj){} obj称之为同步监视器,一般为需要同步的资源,且注意obj是一个不变的资源对象(并非是属性不变,引用不变)
juc包里面有写好了的同步容器。如;CopyOnWriteArrayList

同步带来的死锁问题

死锁:过多的同步可能造成相互不释放资源,从而互相等待,一般发生于同步中持有多个对象的锁。
如何避免:不要在同一个代码块中,同时持有多个对象的锁(不要嵌套使用synchronized)。

生产者和消费者模式:
	共享同一个资源,相互依赖,互为条件。wait和notify方法(Object类的方法)只能在同步方法或块中执行,否则异常。
	方法1:管程法,生产者和消费者之间有一个缓冲器,双方不需要相互打交道,只需要从缓冲区进行操作。
	方法2:信号灯法。根据信号来分别进行操作。适用于一人一下的交互。
wait方法使线程进入阻塞状态,但会释放锁,等待其他线程当前同步块中调用notify方法唤醒。
管程法
package threadtest.locktest;

/**
 * 协作模式,生产者消费者模式实现方式一:管程法。
 * 管程法通过一个缓冲区来实现,生产者和消费者不需要和对方打交道。
 * @author jjh
 */
public class CoTest01 {
    public static void main(String[] args) {
        Container container = new Container();
        Productor productor = new Productor(container);
        Consumer consumer = new Consumer(container);
        productor.start();
        consumer.start();
    }
}

/**
 * 生产者
 */
class Productor extends Thread{
    private Container container;

    public Productor(Container container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生产第" + i + "个馒头");
            container.push(new Steamedbun(i));
        }
    }
}

/**
 * 消费者
 */
class Consumer extends Thread{
    private Container container;

    public Consumer(Container container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费序号为" + container.pop().id 
            	+ "的馒头");
        }
    }
}

/**
 * 缓冲区
 */
class Container {
    /**
     * 给定缓冲区大小
     */
    Steamedbun[] steamedbuns = new Steamedbun[10];
    /**
     * 计数器
     */
    int count = 0;

    /**
     * 生产
     * @param bun
     */
    public synchronized void push(Steamedbun bun){
        //缓冲区已满 停止生产
        if (count == steamedbuns.length){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //开始生产
        steamedbuns[count] = bun;
        count++;
        this.notifyAll();
    }

    /**
     * 消费
     * @return
     */
    public synchronized Steamedbun pop(){
        //缓冲区为空 停止消费
        if (count == 0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count--;
        Steamedbun bun = steamedbuns[count];
        this.notifyAll();
        return bun;
    }

}

/**
 * 面包
 */
class Steamedbun {
    int id;

    public Steamedbun(int id) {
        this.id = id;
    }
}
信号灯法
package threadtest.locktest;

/**
 * 协作模式,生产者消费者模式实现方式一:管程法。
 * @author jjh
 */
public class CoTest02 {
    public static void main(String[] args) {
        Tv tv = new Tv();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

/**
 * 播放节目
 */
class Player extends Thread{
    Tv tv;

    public Player(Tv tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 ==0){
                tv.play("奇葩说");
            }else {
                tv.play("太污了,来包立白洗洗嘴!");
            }

        }
    }
}

/**
 * 观众
 */
class Watcher extends Thread{
    Tv tv;

    public Watcher(Tv tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

/**
 * 电视
 */
class Tv {
    /**
     * 节目
     */
    String voice;

    /**
     * 信号灯
     * true表示演员表演
     * false表示观众观看表演
     */
    boolean flag = true;

    public synchronized void play(String voice){
        //演员等待
        if (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //可以开始表演了
        System.out.println("表演了" + voice);
        this.voice = voice;
        this.notifyAll();
        //标志位改变
        this.flag = !this.flag;
    }

    public synchronized void watch(){
        //观众等待
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //有节目开始观看
        System.out.println("观看了" + voice);
        this.notifyAll();
        //标志位改变
        this.flag = !this.flag;
    }
}
指令重排

volatile
多线程存在指令重排造成代码结果不在期望之中,于是volatile的作用:
  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  2)禁止进行指令重排序。
volatile保证原子性,像轻量级的synchronized.
我们看一下经典的单例模式实现方式之懒汉式双重检查单例模式:

package threadtest.singletest;

/**
 * DCL单例模式: 懒汉式套路基础上加入并发控制,保证在多线程环境下,对外存在一个对象
 * 1、构造器私有化 -->避免外部new构造器
 * 2、提供私有的静态属性 -->存储对象的地址
 * 3、提供公共的静态方法 --> 获取属性
 */
public class DoubleCheckLocking {
    //懒汉式,加上volatile,因为线程创建有三步
    //1、new开辟空间 //2、构造 初始化对象信息 //3、返回对象的地址给引用
    //防止指令重排序,2没执行完3便返回的引用。
    private static volatile DoubleCheckLocking instance;
    //私有的构造函数
    private DoubleCheckLocking(){
    }

    /**
     * 非同步
     * @return
     */
    public static DoubleCheckLocking getInstance1(Long time) {
        if (null == instance){
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new DoubleCheckLocking();
        }
        return instance;
    }

    /**
     * 双重检测的单例模式
     * @return
     */
    public static DoubleCheckLocking getInstance() {
        //非空返回,不走同步块,提高效率
        if (null != instance){
            return instance;
        }
        synchronized (DoubleCheckLocking.class){
            if (null == instance){
                instance = new DoubleCheckLocking();
            }
        }
        return instance;
    }
    public static void main(String[] args) {
//        Thread t = new Thread(()->{
//            System.out.println(DoubleCheckLocking.getInstance());
//        }) ;
//        t.start();
//        System.out.println(DoubleCheckLocking.getInstance());

        Thread t2 = new Thread(()->{
            System.out.println(DoubleCheckLocking.getInstance1(1000L));
        }) ;
        t2.start();
        System.out.println(DoubleCheckLocking.getInstance1(2000L));
    }
}

锁还有一些其他的知识,概述一下:

锁分为两类:
	悲观锁:synchronized是独占锁,会导致其他锁挂起。
	乐观锁:每次不加锁而是假设没有冲突而去完成操作,如果冲突失败就重试,直到成功。
    CAS:Compare and Swap 比较并交换,乐观锁的一种实现,属于硬件级的操作(利用CPU的CAS指令,同时借助JNI来完成非阻塞算法),效率比加锁高。
	当前值V,将更新为的值B,旧的预期值A,通过比较V和A,要是相等就更新值为B,否则返回false,这种比较并交换的方式存在ABA的问题,
	V和A比较并不能断定A没有变化,有可能是A->B->A,可以通过记录日志的方法来解决ABA问题,如果日志改变了说明值是发生过变化的。如AtomicInteger

可重入的锁(juc包ReentrantLock),当线程向一个已经拥有的锁申请锁时是可重入的,并不会因为被自己占用就申请不到而死锁,当重复申请时,锁计数会加一,只有计数为0才释放锁。
yield方法虽然进入就绪状态,但是不会释放锁,容易造成死锁。

结束

在此感谢尚学堂官网的视频,谢谢!
代码我打包了,需要的可以下载,仅供参考!

猜你喜欢

转载自blog.csdn.net/qq_38584967/article/details/87933986
今日推荐