首页 > 编程语言 > 详细

28、多线程(线程安全问题、同步代码块、(静态)同步方法、Lock锁、死锁、wait与notify版的生产者消费者、volatile、CAS算法、原子类、线程状态转换图、匿名内部类创建线程)

时间:2020-06-06 17:04:40      阅读:61      评论:0      收藏:0      [点我收藏+]

线程安全问题

引入

售票案例

package org.westos.demo3;

/**
 * 模拟实际售票场景
 * @author lwj
 * @date 2020/6/4 20:07
 */
public class MyTest3 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread a = new Thread(myRunnable, "a");
        Thread b = new Thread(myRunnable, "b");
        Thread c = new Thread(myRunnable, "c");
        a.start();
        b.start();
        c.start();
    }
}

class MyRunnable implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets >= 1) {
                try {
                    Thread.sleep(50);
                    //判断有票之后
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "窗口出售了第" + tickets-- + "张票");
            } else {
                break;
            }
        }
    }
}
  • 出现第0张票或者负数票:由于线程的随机性导致

当余票只剩下一张时,此时a线程抢到了CPU执行权,判断tickets>=1后进行休眠状态,此时b线程抢到了CPU执行权,if判断为true,进行休眠状态,此时c线程抢到了CPU执行权,然后进行休眠状态,然后等a,b,c线程休眠完毕后,卖出的票为第1张,第0张,第-1张票。

  • 出现相同票:由于线程的原子性导致

原子性,不可分割性。

线程对tickets--不是一个原子性操作,线程要对tickets变量进行读、改、写操作。

Java内存模型:tickets变量存在于主存,每个线程需要此变量时,需要把该变量读取到各自的工作内存。

所以,当a线程读取到100,然后改为99时,还未写回主内存,b线程读取了该100,改为99,所以这就导致了出现相同票的情况。

出现线程安全问题,符合三个条件:

  • 多线程环境
  • 多个线程有没有共享数据
  • 有没有多条语句在操作这个共享变量(tickets--)

打破这种情况,就需要破坏三个条件中的一个,所以我们需要破坏第三种情况。

用一把锁把出现安全问题的代码锁起来(同步代码块)

同步代码块

synchronized(锁) {
	有可能出现线程安全问题的代码
}

锁:你可以用Java中的一个对象来充当,但是,多个线程必须用同一个对象(同一把锁)。
package org.westos.demo4;

/**
 * 模拟实际售票场景
 * @author lwj
 * @date 2020/6/4 20:07
 */
public class MyTest3 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread a = new Thread(myRunnable, "a");
        Thread b = new Thread(myRunnable, "b");
        Thread c = new Thread(myRunnable, "c");
        a.start();
        b.start();
        c.start();

        /*
        a窗口出售了第100张票
        c窗口出售了第99张票
        c窗口出售了第98张票
        c窗口出售了第97张票
        c窗口出售了第96张票
        c窗口出售了第95张票
        c窗口出售了第94张票
        c窗口出售了第93张票
        b窗口出售了第92张票
        b窗口出售了第91张票
        b窗口出售了第90张票
         */
    }
}

class MyRunnable implements Runnable {
    private int tickets = 100;
    private static Object object = new Object();
    //确保这个锁对象只有一个,多个线程共享一把锁

    @Override
    public void run() {
        while (true) {
            synchronized (object) {
                //当a线程抢到了这个锁后,其他线程就需要在同步代码块外面等待
                //当a线程执行完毕,出了同步代码块,然后所有线程重新抢占
                //也就是说,同步代码块中的代码是单线程,任何时候都只有一个线程在操作
                if (tickets >= 1) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "窗口出售了第" + tickets-- + "张票");
                } else {
                    break;
                }
            }
        }
    }
}

Java的内置锁:每个Java对象都可以用做一个实现同步的锁,这些锁成为内置锁。

线程进入同步代码块或者同步方法时,会自动获得该锁,在退出同步代码块或者同步方法时,会释放该锁。

获得内置锁的唯一途径就是进行这个锁保护的同步代码块或者同步方法,Java内置锁是一个互斥锁,意味着最多只有一个线程能够获得该锁。

当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果线程B不释放这个锁,那么线程A将永远等待下去。

同步方法

package org.westos.demo;

/**
 * @author lwj
 * @date 2020/6/6 10:06
 */
public class MyTest {
    public static void main(String[] args) {
        CellRunnable cellRunnable = new CellRunnable();
        Thread a = new Thread(cellRunnable, "A");
        Thread b = new Thread(cellRunnable, "B");
        Thread c = new Thread(cellRunnable, "C");
        a.start();
        b.start();
        c.start();
    }
}

class CellRunnable implements Runnable {
    private int tickets = 100;
    public static Object object = new Object();

    @Override
    public void run() {
        while (tickets >= 1) {
            cell();
        }
    }

    //同步方法,用的锁对象是this对象,因为只有一个cellRunnable对象,所以org.westos.demo.CellRunnable@6720b744
    public synchronized void cell() {
        if (tickets >= 1) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this);
            System.out.println(Thread.currentThread().getName() + "窗口卖出了第" + tickets-- + "张票。");
        }
    }
}

静态的同步方法

package org.westos.demo2;

/**
 * @author lwj
 * @date 2020/6/6 10:06
 */
public class MyTest {
    public static void main(String[] args) {
        CellRunnable cellRunnable = new CellRunnable();
        Thread a = new Thread(cellRunnable, "A");
        Thread b = new Thread(cellRunnable, "B");
        Thread c = new Thread(cellRunnable, "C");
        a.start();
        b.start();
        c.start();
    }
}

class CellRunnable implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            synchronized (CellRunnable.class) { //(this == 同步方法)
                if (tickets >= 1) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "窗口卖出了第" + tickets-- + "张票。");
                } else {
                    break;
                }
            }
        }
    }
}
package org.westos.demo2;

/**
 * @author lwj
 * @date 2020/6/6 10:06
 */
public class MyTest {
    public static void main(String[] args) {
        CellRunnable cellRunnable = new CellRunnable();
        Thread a = new Thread(cellRunnable, "A");
        Thread b = new Thread(cellRunnable, "B");
        Thread c = new Thread(cellRunnable, "C");
        a.start();
        b.start();
        c.start();
    }
}

class CellRunnable implements Runnable {
    private static int tickets = 100;

    @Override
    public void run() {
        while (tickets >= 1) {
            cell();
        }
    }

    //同步静态方法所用的锁为类Class对象
    public synchronized static void cell() {
        if (tickets >= 1) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "窗口卖出了第" + tickets-- + "张票。");
        }
    }
}

Java的对象锁和类锁:在锁的概念上,基本上和内置锁是一致的,但是对象锁是用于实例方法,类锁是用于类的静态方法。

不同对象实例(Runnable子类对象)的对象锁是互不干扰的,但是每个类只有一个类锁。

其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。

Lock锁

在JDK 1.5之后引入了不同于synchronized关键字的Lock系列锁。

package org.westos.demo3;


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

/**
 * Lock锁
 * @author lwj
 * @date 2020/6/6 10:32
 */
public class MyTest {
    public static void main(String[] args) {
        CellRunnable cellRunnable = new CellRunnable();
        Thread a = new Thread(cellRunnable, "A");
        Thread b = new Thread(cellRunnable, "B");
        Thread c = new Thread(cellRunnable, "C");
        a.start();
        b.start();
        c.start();
    }
}

class CellRunnable implements Runnable {
    private int tickets = 100;
    public static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                if (tickets >= 1) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "窗口卖出了第" + tickets-- + "张票。");
                } else {
                    break;
                }
            } finally {
                lock.unlock();
                //确保释放锁的操作要执行
            }
        }
    }
}

线程安全的类

  • 线程安全:就是当多线程访问时,采用了加锁的机制;即当一个线程访问该类的某个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读写完之后,其他线程才可以使用。防止出现数据不一致或者数据被污染的情况。
  • 线程不安全:就是不提供数据访问时的数据保护,多个线程能够同时操作某个数据,从而出现数据不一致或者数据污染的情况。
  • 对于线程不安全的问题,一般会使用synchronized关键字加锁同步控制。

Vector、StringBuffer、Hashtable等都属于线程安全的类,其方法都有synchronized修饰,都属于同步方法。

package org.westos.demo4;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 线程安全与线程不安全的集合
 * @author lwj
 * @date 2020/6/6 11:00
 */
public class MyTest {
    public static void main(String[] args) {
        /*
        Vector:线程安全,方法大多都是同步方法
        StringBuffer:线程安全
        StringBuilder:线程不安全
        ArrayList:线程不安全
        
        Hashtable:线程安全
         */

        ArrayList<String> strings = new ArrayList<>();
        List<String> stringList = Collections.synchronizedList(strings);
        //stringList线程安全的List集合
    }
}

死锁

package org.westos.demo4;

/**
 * 死锁
 * @author lwj
 * @date 2020/6/6 11:10
 */
public class MyTest2 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread(true);
        MyThread myThread1 = new MyThread(false);
        myThread.start();
        myThread1.start();
    }
}

class MyThread extends Thread {
    boolean flag;

    public MyThread(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (ObjectUtils.objA) {
                System.out.println(flag + "线程持有了objA锁,现在尝试获取objB锁");
                try {
                    Thread.sleep(50);
                    //确保B线程获取了objB锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (ObjectUtils.objB) {
                    System.out.println(flag + "线程持有了objB锁");
                }
            }
        } else {
            synchronized (ObjectUtils.objB) {
                System.out.println(flag + "线程持有了objB锁,现在尝试获取objA锁");
                try {
                    Thread.sleep(50);
                    //确保A线程获取了objA锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (ObjectUtils.objA) {
                    System.out.println(flag + "线程持有了objA锁");
                }
            }
        }
    }
}

interface ObjectUtils {
    Object objA = new Object();
    Object objB = new Object();
}

线程间的等待与唤醒

生产者与消费者案例

前面所学的线程都是同一种类的线程,现在学习两种不同种类的线程,一个是生产者线程,另一个是消费者线程。

这就会涉及不同种类的线程之间通信问题。

资源类

package org.westos.demo5;

/**
 * 面包:资源类
 * @author lwj
 * @date 2020/6/6 13:48
 */
public class Bread {
    public String name;
    public boolean flag;
}

生产者线程

package org.westos.demo5;

/**
 * @author lwj
 * @date 2020/6/6 13:48
 */
public class Producer extends Thread {
    int i = 1;
    public Bread bread;

    public Producer(Bread bread) {
        this.bread = bread;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (bread) {
                if (bread.flag) {
                    //如果有资源,那么等待,让消费者消费
                    try {
                        bread.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //如果没有资源
                bread.name = "馒头" + i++ + "号";
                System.out.println(bread.name + "已经生产好了");
                bread.flag = true;
                //通知消费者开始消费
                bread.notifyAll();
            }
        }
    }
}

消费者线程

package org.westos.demo5;

/**
 * @author lwj
 * @date 2020/6/6 13:48
 */
public class Customer extends Thread {

    //消费者和生产者需要共享变量
    public Bread bread;

    public Customer(Bread bread) {
        this.bread = bread;
    }

    @Override
    public void run() {
        while (true) {
            //while(true)的作用就是当该线程抢到了CPU,并且执行一遍逻辑后,时间片还有,所以可以执行多次
            synchronized (bread) {
                if (!bread.flag) {
                    //如果没有资源,那么消费者线程需要等待
                    try {
                        bread.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //如果有资源,开始消费
                System.out.println(bread.name + "已经被消费了");
                //现在没有了资源
                bread.flag = false;
                //通知生产者线程开始生产
                bread.notifyAll();
                //唤醒在此对象监视器上等待的所有线程
                //或者notify():唤醒在此对象监视器上等待的单个线程
            }
        }
    }
}

测试类

package org.westos.demo5;

/**
 * 生产者与消费者
 * @author lwj
 * @date 2020/6/6 13:29
 */
public class MyTest {
    public static void main(String[] args) {
        Bread bread = new Bread();
        //开始都是默认值String,null,flag,false
        Customer customer = new Customer(bread);
        Producer producer = new Producer(bread);
        //两个线程共享一个变量
        producer.start();
        customer.start();
    }
}

打印日志:

馒头1号已经生产好了
馒头1号已经被消费了
馒头2号已经生产好了
馒头2号已经被消费了
馒头3号已经生产好了
馒头3号已经被消费了
馒头4号已经生产好了
馒头4号已经被消费了
馒头5号已经生产好了
馒头5号已经被消费了

wait和sleep的区别

共同点:

wait()和sleep(long millis)都会导致当前线程的阻塞;

不同点:

wait()可以不指定时间量,sleep必须指定时间量;

wait()会导致当前线程释放锁,而sleep不会导致当前线程释放锁。

内存可见性(volatile)

package org.westos.demo6;

/**
 * @author lwj
 * @date 2020/6/6 15:03
 */
public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable myRunnable = new MyRunnable();
        Thread a = new Thread(myRunnable, "A");
        a.start();
        //Thread.sleep(2000);
        //如果main线程不休眠2s,那么main线程从主内存拿到的flag一直为false,所以if循环进不去,不可能退出while循环
        //1、如果main线程休眠2s,等待A线程修改了flag,并写回主存,等到main线程读取flag时已经是true了
        while (true) {
            if (myRunnable.flag) {
                System.out.println("执行了");
                break;
            }
        }
    }
}

class MyRunnable implements Runnable {
    //3、volatile保证内存可见性,某个线程修改了某个共享变量后,会立即写回主存
    public volatile boolean flag = false;

    /*
    JAVA内存模型:
    成员变量存储在主存中,每个线程都有自己的工作内存,它们要去操作主存中的数据,必须把主存中的数据读取到自己的工作内存中,操作完后再写回主存
     */

    public boolean isFlag() {
        return flag;
    }

    @Override
    public void run() {
        //2、synchronized,flag被修改后会立即刷新回主存,保证内存可见性
        synchronized (MyRunnable.class) {
            flag = true;
            System.out.println(Thread.currentThread().getName() + "线程执行了,此时flag为" + flag);
        }
    }
}

CAS算法

CAS(Compare And Swap),比较并交换。

是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并
发访问。

CAS 是一种无锁的非阻塞算法的实现。

CAS 包含了 3 个操作数:

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

当且仅当V的值等于A时,CAS 通过原子方式用新值B来更新V的值,否则不会执行任何操作。

Java 5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

package org.westos.demo6;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author lwj
 * @date 2020/6/6 15:30
 */
public class MyTest2 {
    public static void main(String[] args) {
        CellRunnable cellRunnable = new CellRunnable();
        for (int i = 0; i < 5; i++) {
            new Thread(cellRunnable, String.valueOf(i)).start();
        }
    }
}

class CellRunnable implements Runnable {
    //舍弃synchronized,使用CAS原理的原子类
    public AtomicInteger tickets = new AtomicInteger(100);

    @Override
    public void run() {
        while (true) {
            if (tickets.get() >= 1) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "线程销售了第" + tickets.getAndDecrement() + "张票");
            } else {
                break;
            }
        }
    }
}

线程状态转换图

技术分享图片

匿名内部类---线程

package org.westos.demo7;

/**
 * 匿名内部类
 * @author lwj
 * @date 2020/6/6 16:42
 */
public class MyTest {
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                System.out.println("匿名内部类继承Thread创建线程");
            }
        }.start();
        //使用匿名内部类的方式创建线程

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类实现Runnable接口创建线程");
            }
        }).start();
    }
}

28、多线程(线程安全问题、同步代码块、(静态)同步方法、Lock锁、死锁、wait与notify版的生产者消费者、volatile、CAS算法、原子类、线程状态转换图、匿名内部类创建线程)

原文:https://www.cnblogs.com/shawnyue-08/p/13055348.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!