Java-多线程(三)-线程同步、多线程并发案例、线程同步的加锁方法、死锁

Java-多线程-3

学习视频:B站 狂神说Java – https://www.bilibili.com/video/BV1V4411p7EF

学习博客:csdn – https://blog.csdn.net/qijingwang/article/details/104892717

学习博客:csdn – https://blog.csdn.net/yalu_123456/article/details/91050036?spm=1001.2014.3001.5501

8、线程同步规则

8.1、线程简介及多线程的影响

我们先回顾一下线程。

线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个程序至少有一个进程。线程是CPU调度和执行的单位

一个进程中会有一个或多个线程,这些线程会共享进程的资源,但又有各自的程序计数器、栈和局部变量表。同一进程中的不同线程能够访问相同的变量,并且在同一个堆上分配对象。这样就会产生不同的正负作用影响。

优势(正作用):

  • 提高程序的运行性能。
  • 充分利用系统的处理能力,提高系统的资源利用率。
  • 提高系统响应性,即线程可以在运行现有任务的情况下立即开始处理新的任务。

问题(负作用):

  • 安全性问题:即并发问题,共享进程资源,访问相同变量,却有各自的程序计数器、栈和局部变量表, 这会造成对同一对象分配处理时,状态变量改变了 或者 没有及时改变,发生数据紊乱现象。
  • 活跃性问题:不正确的加锁、解锁方式可能会导致死锁or活锁问题。
  • 性能问题:多线程并发即多个线程切换运行,线程切换会有一定的消耗并且不正确的加锁。

8.2、线程同步规则

并发

并发:同一个对象被多个线程同时操作。 这样造成了数据紊乱。如下图例:
在这里插入图片描述

线程同步

现实生活中,我们会遇到 ” 同一个资源 , 多个人都想使用 ” 的问题 , 比如,食堂排队打饭 , 每个人都想吃饭 , 最天然的解决办法就是 , 排队 . 一个个来.

在这里插入图片描述

这种所有人都去共享 饭资源,就好比是多线程。 在处理多线程并发问题时,我们可以借鉴排队打饭这种方法。 多个线程访问同一个对象 , 并且某些线程还想修改这个对象 .这时候我们就需要线程同步 . 线程同步其实就是一种等待机制 , 多个需要同时访问此对象的线程进入这个对象的等待池 形成队列, 等待前面线程使用完毕 , 下一个线程再使用

线程同步队列和锁:由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题。为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:

  • 一个线程持有锁会导致其它所有需要此锁的线程挂起;

  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;

  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。

多个线程访问同一对象,造成并发问题。

队列和锁 才能去完成线程同步,解决线程并发的安全问题,但是性能就会差。

9、三大不安全案例

9.1、不安全的买票案例

在买票的时候, 多个线程去对买票这个进程中的共享资源 票数 进行共享,但是由于每个线程有各自的程序计数器、虚拟机栈和本地方法栈、局部变量表,导致数据资源改变,状态不一致,造成了数据紊乱。

我们采用sleep()方法模拟延时,放大问题的发生性。

例子:不安全的买票,代码:

package com.AL.Multithread;

// 线程不安全,数据紊乱。
// 不安全的买票。 会出现票数为负
public class UnsafeBuyTicket {
    public static void main(String[] args){
        BuyTicket station = new BuyTicket();

        new Thread(station,"可怜的我").start();
        new Thread(station,"得意的你").start();
        new Thread(station,"可恶的黄牛党").start();
    }
}

class BuyTicket implements Runnable{
    // 票数
    private int ticketNums = 10;
    boolean flag = true; // 外部停止方式,标志位
    @Override
    public void run() {
        // 买票
        while (flag){
            try {
                buy();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    // 买票,这一部分是为了去改变 flag,便于线程停止
    private void buy() throws InterruptedException{
        // 判断是否有票
        if (ticketNums <= 0){
            flag = false;
            return;
        }
        //模拟延时。  可以放大  问题的发生性
        Thread.sleep(100);
        //买票
        System.out.println(Thread.currentThread().getName()+"拿到了"+ticketNums--);
    }
}

结果:票数出现重复,有时为负数。这种错误的原因是:每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。 这就是线程不安全的一个例子。

得意的你拿到了8
可怜的我拿到了9
可恶的黄牛党拿到了10
可怜的我拿到了7
得意的你拿到了5
可恶的黄牛党拿到了6
得意的你拿到了4
可恶的黄牛党拿到了4
可怜的我拿到了3
得意的你拿到了2
可怜的我拿到了1
可恶的黄牛党拿到了2

9.2、不安全的取钱案例

不安全的取钱案例: 并发问题,数据紊乱,线程不安全。

对于银行这个类来讲,会有账户;会有两个人去取钱,即对应着 多个线程对同一个资源对象进行操作。而这两个人即两个线程会由于各自本身具有程序计数器,内存空间,导致这同一个对象分别在两个线程中的状态变量不能同步更新, 从而造成数据紊乱。

代码:

package com.AL.Multithread;

//不安全的取钱
// 两个人去银行取钱, 账户
public class UnsafeBank {
    public static void main(String[] args){
        //账户
        Account account = new Account(100,"结婚基金");
        Drawing you=new Drawing(account,50,"你");
        Drawing girlFriend=new Drawing(account,100,"女朋友");
        you.start();
        girlFriend.start();
    }
}
//账户
class Account{
    int money; //余额
    String name; //卡名

    public Account(int money, String name){
        this.money = money;
        this.name = name;
    }
}
//银行: 模拟取款
class Drawing extends Thread{
    Account account; //账户
    int drawingMoney; //取了多少钱
    int nowMoney; // 现在卡里还有多少钱

    public Drawing(Account account, int drawingMoney, String name){
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }
    //取钱
    @Override
    public void run() {
        //判断有没有钱
        if (account.money - drawingMoney < 0){
            System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
            return;
        }
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 卡内余额 = 余额-你取的钱
        account.money = account.money - drawingMoney;
        // 你手里的钱
        nowMoney= nowMoney + drawingMoney;

        System.out.println(account.name+"余额为:"+account.money);
        System.out.println(this.getName()+"手里的钱:"+nowMoney);
    }
}

结果:这出现错误,账户金额为负数。

结婚基金余额为:-50
结婚基金余额为:-50
女朋友手里的钱:100
你手里的钱:50

9.3、不安全的集合案例

例子:

package com.AL.Multithread;

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

public class UnSafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();

        for (int i = 0; i < 100000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }

        for (int i = 5;i>0;i--){
            Thread.sleep(1000);
            System.out.println("倒计时"+i);
        }

        System.out.println(list.size());
    }
}

结果: 结果中没有到达100000,线程不安全。

倒计时5
倒计时4
倒计时3
倒计时2
倒计时1
99974

10、同步方法及同步块

10.1、同步方法

同步方法:

  • 由于我们可以通过 private 关键字来保证数据对象只能被方法访问 , 所以我们只需要针对方法提出一套机制 , 这套机制就是 synchronized 关键字 , 它包括两种用法 :synchronized 方法 和synchronized 块
    同步方法 : public synchronized void method(int args) {}

  • synchronized方法控制对 “对象” 的访问 , 每个对象对应一把锁 , 每个synchronized方法都必须获得调用该方法的对象的锁才能执行 , 否则线程会阻塞 ;
    方法一旦执行 , 就独占该锁 , 直到该方法返回才释放锁 , 后面被阻塞的线程才能获得这个锁 , 继续执行;
    缺陷 : 若将一个大的方法申明为synchronized 将会影响效率

同步方法的弊端
方法里面需要修改的内容才需要锁,
锁的太多 , 浪费资源

10.2、同步块:

  • 同步块 : synchronized (Obj ) { }

  • Obj 称之为 同步监视器

    • Obj 可以是任何对象 , 但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器 , 因为同步方法的同步监视器就是this , 就是这个对象本身 , 或者是 class [ 反射中讲解 ]
  • 同步监视器的执行过程

    1. 第一个线程访问 , 锁定同步监视器 , 执行其中代码 .
    2. 第二个线程访问 , 发现同步监视器被锁定 , 无法访问 .
    3. 第一个线程访问完毕 , 解锁同步监视器 .
    4. 第二个线程访问, 发现同步监视器没有锁 , 然后锁定并访问

10.3、加锁的买票案例

使用synchronized 方法去控制对"对象"的访问, 此时对 买票 这个方法进行加锁.

那么从此时对buy()方法添加关键词synchronized 后为 private synchronized void buy() 相当于锁住。

同步方法避免了线程不安全的问题,如数据紊乱的问题避免了。

package com.AL.Multithread;

// 线程不安全,数据紊乱。
// 不安全的买票。 会出现票数为负
public class UnsafeBuyTicket {
    public static void main(String[] args){
        BuyTicket station = new BuyTicket();

        new Thread(station,"可怜的我").start();
        new Thread(station,"得意的你").start();
        new Thread(station,"可恶的黄牛党").start();
    }
}

class BuyTicket implements Runnable{
    // 票数
    private int ticketNums = 10;
    boolean flag = true; // 外部停止方式,标志位
    @Override
    public void run() {
        // 买票
        while (flag){
            try {
                buy();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    // 买票,这一部分是为了去改变 flag,便于线程停止
    //private void buy() throws InterruptedException{
    private synchronized void buy() throws InterruptedException{//添加关键词synchronized,相当于锁住,同步方法.避免了线程不安全的问题,如数据紊乱的问题避免了
        // 判断是否有票
        if (ticketNums <= 0){
            flag = false;
            return;
        }
        //模拟延时。  可以放大  问题的发生性
        Thread.sleep(100);
        //买票
        System.out.println(Thread.currentThread().getName()+"拿到了"+ticketNums--);
    }
}

结果:

可怜的我拿到了10
可怜的我拿到了9
可怜的我拿到了8
可怜的我拿到了7
可恶的黄牛党拿到了6
可恶的黄牛党拿到了5
得意的你拿到了4
得意的你拿到了3
得意的你拿到了2
得意的你拿到了1

10.4、加锁的银行取钱案例

在取钱的这个例子中, 我们能直接对取钱的 run 方法这里添加关键词后为: public synchronized void run() , 但是结果仍然有问题, 因为在这个run方法中,默认锁住的是 this。

取钱的例子结果表明,我们没有完全锁住。 我们应该去锁住 account这个账户,修改如下:即采用synchronized(Obj)块去解决。锁的对象应该是 增删查改的对象,即不是锁银行,应该是锁账户。

package com.AL.Multithread;

//不安全的取钱
// 两个人去银行取钱, 账户
public class UnsafeBank {
    public static void main(String[] args){
        //账户
        Account account = new Account(100,"结婚基金");
        Drawing you=new Drawing(account,50,"你");
        Drawing girlFriend=new Drawing(account,100,"女朋友");
        you.start();
        girlFriend.start();
    }
}
//账户
class Account{
    int money; //余额
    String name; //卡名

    public Account(int money, String name){
        this.money = money;
        this.name = name;
    }
}
//银行: 模拟取款
class Drawing extends Thread{
    Account account; //账户
    int drawingMoney; //取了多少钱
    int nowMoney; // 现在卡里还有多少钱

    public Drawing(Account account, int drawingMoney, String name){
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }
    //取钱
    @Override
    public void run() {
        synchronized (account) { //Synchronized(Obj){}同步块,锁住增删查改的对象
            //判断有没有钱
            if (account.money - drawingMoney < 0) {
                System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
                return;
            }
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            // 卡内余额 = 余额-你取的钱
            account.money = account.money - drawingMoney;
            // 你手里的钱
            nowMoney = nowMoney + drawingMoney;

            System.out.println(account.name + "余额为:" + account.money);
            System.out.println(this.getName() + "手里的钱:" + nowMoney);
        }
    }
}

结果:

结婚基金余额为:50
你手里的钱:50
女朋友钱不够,取不了

10.5、集合的案例

JUC并发编程,保护线程安全的一些类。

package com.AL.Multithread;

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

public class SafeList {
    public static void main(String[] args) throws InterruptedException {

        List<String> list = new ArrayList<String>();

        for (int i = 0; i < 100000; i++) {
            new Thread(()->{
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }

        for (int i = 5;i>0;i--){
            Thread.sleep(1000);
            System.out.println("倒计时"+i);
        }
        System.out.println(list.size());
    }
}

或者我们可以选择能够保证线程安全的List, 即使用 CopyOnWriteArrayList。如下所示:

package com.AL.Multithread;

import java.util.concurrent.CopyOnWriteArrayList;

public class SafeJUCList {
    public static void main(String[] args) throws InterruptedException {
        //保证线程安全的list , ArrayList
        CopyOnWriteArrayList list = new CopyOnWriteArrayList();

        for (int i = 0; i < 100000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }

        for (int i = 5;i>0;i--){
            Thread.sleep(1000);
            System.out.println("倒计时"+i);
        }
        System.out.println(list.size());
    }
}

上述两种方法都能解决集合List的线程安全问题,结果如下:

倒计时5
倒计时4
倒计时3
倒计时2
倒计时1
100000

11、死锁

死锁

  • 多个线程各自占有一些共享资源 , 并且互相等待其他线程占有的资源才能运行 , 而导致两个或者多个线程都在等待对方释放资源 , 都停止执行的情形 。某一个同步块同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 “ 死锁 ” 的问题 .

两个或多个线程都在等待对方释放资源,结果产生了都停止执行的情形,即死锁

产生死锁的四个必要条件

  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

死锁避免方法:我们只要将这四个必要条件破坏掉任意一个就可以避免死锁发生。

例子:多个线程去共用同一个资源,多个线程互相拥抱着对方需要的资源,然后形成僵持,造成了死锁。

在灰姑娘或者白雪公主任意一个进程去进行多线程时, 因为它们各自都会紧握着对方需要的那个锁的资源,你在抱着这两个锁的资源时,还想让对方给你想要的,对方并没有得到。

拿了镜子的锁,里面还包含想要口红的锁;但是有了口红的锁,还包含着镜子的锁,这就造成了同一资源被两个进程调用。所以就造成了死锁。

为了避免死锁,白雪公主或者灰姑娘,在已经拥有一个锁的时候,就把另外一个锁放出去,然后等待得到自己想要的锁 资源,就能避免死锁了。

在代码中,注释部分是死锁的案例,即同一资源被两个进程去调用。 将锁修改到外面的代码块, 不让他抱对方的锁,就可以解决问题。

package com.AL.Multithread;

//死锁: 多个线程共用同一个资源。
// 多个线程互相拥抱着对方需要的资源,然后形成僵持
public class DeadLock {
    public static void main(String[] args){
        Makeup g1 = new Makeup(0,"灰姑娘");
        Makeup g2 = new Makeup(1,"白雪公主");

        g1.start();
        g2.start();
    }
    }

//口红
class Lipstick{

}
//镜子
class Mirror{

}

class Makeup extends Thread {
    //需要的资源只有一份,用static来保证只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice; //选择
    String girlNmae; //使用化妆品的人

    Makeup(int choice, String girlNmae) {
        this.choice = choice;
        this.girlNmae = girlNmae;
    }

    @Override
    public void run() {
        //化妆
        try {
            makeup();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
/**
    //此时, 同一个资源被两个进程调用,造成了死锁
    private void makeup() throws InterruptedException {
        if (choice == 0) {
            synchronized (lipstick) { //设置口红的锁, 同步块
                System.out.println(this.girlNmae + "获得口红的锁");
                Thread.sleep(1000);
                synchronized (mirror) { // 一秒钟后想要获得镜子
                    System.out.println(this.girlNmae + "获得镜子的锁");
                }
            }
        } else {
            synchronized (mirror) { // 获得镜子的锁
                System.out.println(this.girlNmae + "获得镜子的锁");
                Thread.sleep(1000);
                synchronized (lipstick) { // 一秒钟后想要获得口红
                    System.out.println(this.girlNmae + "获得口红的锁");
                }
            }
        }
    }
 */
private void makeup() throws InterruptedException {
    if (choice == 0) {
        synchronized (lipstick) { //设置口红的锁, 同步块
            System.out.println(this.girlNmae + "获得口红的锁");
            Thread.sleep(1000);
        }
        synchronized (mirror) { // 一秒钟后想要获得镜子
            System.out.println(this.girlNmae + "获得镜子的锁");
        }
    } else {
        synchronized (mirror) { // 获得镜子的锁
            System.out.println(this.girlNmae + "获得镜子的锁");
            Thread.sleep(1000);
        }
        synchronized (lipstick) { // 一秒钟后想要获得口红
            System.out.println(this.girlNmae + "获得口红的锁");
        }
    }
}
}

结果:

白雪公主获得镜子的锁
灰姑娘获得口红的锁
白雪公主获得口红的锁
灰姑娘获得镜子的锁

12、Lock(锁)

Lock锁

  • JDK5.0开始,java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁

对于Lock(锁),它的语法如下,特别注意的是显示加锁和释放锁

class A{
    private final ReentrantLock lock = new ReenTrantLock();
    public void m(){
        lock.lock(); //加锁
        try{
            //保证线程安全的代码;
        }
        finally{
            lock.unlock(); //解锁
            //如果同步代码有异常,要将unlock()写入finally语句块
        }
}

synchronized 与 Lock 的对比

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

synchronized 与 Lock优先使用顺序

Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)

ReentrantLock类实现了Lock,其中的ReentrantLock称为可重入锁

下方的例子:买票的案例,注释部分为没有加锁的部分,多线程会发生并发问题,造成数据紊乱。 修改后的代码中,使用了ReentrantLock类,实例化对象为 lock, 然后进行加锁和解锁,保证线程安全性。

package com.AL.Multithread;

import java.util.concurrent.locks.ReentrantLock;

//测试Lock锁
// 使用ReentrantLock类实现了Lock
public class TestLock {
    public static void main(String[] args){
        TestLock2 testLock2 = new TestLock2();

        new Thread(testLock2).start();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
    }
}
/**
class TestLock2 implements Runnable{
    int ticketNums = 10;
    @Override
    public void run() {
        while (true){
            if(ticketNums > 0){
                try{
                   Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(ticketNums--);
            }else {
                break;
            }
        }
    }
}
 */
class TestLock2 implements Runnable {
    int ticketNums = 10;

    // 定义lock锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock(); // 加锁
                if (ticketNums > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ticketNums--);
                } else {
                    break;
                }
            } finally {
                //解锁
                lock.unlock();
            }
        }
    }
}

结果:

10
9
8
7
6
5
4
3
2
1
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值