Java多线程(下)

线程同步

场景:目前电影院有100张票,分3个窗口同时买票

public class SellTicket implements Runnable {
    // 定义100张票
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            // t1,t2,t3三个线程
            // 这一次的tickets = 1;
            if (tickets > 0) {
                // 为了模拟更真实的场景,我们稍作休息
                try {
                    Thread.sleep(100); //t1进来了并休息,t2进来了并休息,t3进来了并休息,
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + "正在出售第"
                        + (tickets--) + "张票");
                //窗口1正在出售第1张票,tickets=0
                //窗口2正在出售第0张票,tickets=-1
                //窗口3正在出售第-1张票,tickets=-2
            }
        }
    }
}

public class SellTicketDemo {
    public static void main(String[] args) {
        // 创建资源对象
        SellTicket st = new SellTicket();

        // 创建三个线程对象
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

输出:
窗口1正在出售第100张票
窗口3正在出售第100张票
窗口2正在出售第99张票
窗口1正在出售第98张票
窗口3正在出售第97张票
...
...
窗口1正在出售第3张票
窗口2正在出售第2张票
窗口3正在出售第1张票
窗口2正在出售第0张票
窗口1正在出售第-1张票

结果可以看出,即有同时卖一张票的,还有卖负数票的情况。

首先,我们需要知道 CPU的每一次执行必须是一个原子性(最简单基本的)的操作tickets--其实分为三个原子操作:

  1. 取并记录 tickets 的值
  2. tickets 的值减去1
  3. 返回以前的值。

同时卖一张票情况分析:

  1. 线程 t1、t3 同时进入run()方法并都稍作休息,然后线程 t1 抢占到 CPU 时间片,执行tickets--的第一个原子操作记录 tickets 值为 100;
  2. 而这时线程 t3 抢占到 CPU 时间片,也执行 tickets-- 的第一个原子操作记录 tickets 值也为 100。所以会输出 “窗口1正在出售第100张票、窗口3正在出售第100张票”。

卖负数票情况分析:

  1. 线程t1、t2、t3同时进入run()方法并都稍作休息,然后线程 t3 抢占到 CPU 时间片,执行tickets--的所有原子操作,输出“窗口3正在出售第1张票”,此时 tickets 的值为 0。
  2. 这时线程 t2 抢占到 CPU 时间片,执行tickets--的所有原子操作,输出“窗口2正在出售第0张票”,此时tickets的值为 -1。
  3. 这时线程 t1 抢占到 CPU 时间片,执行tickets--的所有原子操作,输出“窗口1正在出售第-1张票”。

所以卖票的类线程不安全,判断一个程序是否有线程安全问题的依据:

  • 是否有多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

解决方案:
需要将操作共享数据的代码进行封装,保证单个线程执行过程中,其他线程不能执行。Java提供了:同步机制synchronized

  1. 同步代码块
    这里的锁对象可以是任意对象,作用对象是括号括起来的对象实例
synchronized (对象) {
   需要被同步的代码;
}
  1. 同步方法
    把同步加在方法上。这里的锁对象是this,作用对象是调用这个方法的对象
synchronized 返回类型 method() { 
   需要被同步的代码;
}
  1. 静态同步方法
    把同步加在方法上。这里的锁对象是当前类的字节码文件对象,作用对象是这个类的所有对象,相当于该类的一个全局锁
private static synchronized 返回类型 method() {
   需要被同步的代码;
}

同步代码块

package 多线程;

public class SellTicket implements Runnable {
    // 定义100张票
    private int tickets = 100;

    //创建锁对象
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "正在出售第" + (tickets--) + "张票");
                }
            }
        }
    }
}

输出:
线程安全

线程 t1、t2、t3 都走到同步代码块。假设 t1 抢到 CPU 的执行权,t1 就进去执行;而线程 t2 抢到 CPU 的执行权,发现代码块是锁定状态,只能等待。线程 t1 执行完成后,释放锁,然后大家再抢占锁。这样,保证同步代码块内执行只会有一个线程。

注意:锁对象一定要是同一个对象,如果synchronized (new Object()) { ... },还是线程不安全。所以,一般锁对象都是类成员变量

同步方法

public class SellTicket implements Runnable {
    // 定义100张票
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            sellTicket();
        }
    }

    private synchronized void sellTicket() {
        if (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + "正在出售第" + (tickets--) + "张票 ");
        }
    }
}

输出:
线程安全

定义同步方法时,我们并没有声明锁对象,而锁对象是保证线程安全的必要条件。那么同步方法的锁对象是什么呢?

我们可以做个实验,定义一个递增变量 i,用于分发调用不同代码。代码1使用锁对象是成员对象obj,代码2使用的锁对象未知。

public class SellTicket implements Runnable {
    // 定义100张票
    private int tickets = 100;

    //创建锁对象
    private Object obj = new Object();

    int i = 0;

    @Override
    public void run() {
        while (true) {
            if (i%2 == 0) {
                synchronized (obj) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + "正在出售第" + (tickets--) + "张票");
                    }
                }
            } else {
                sellTicket();
            }
            i++;
        }
    }

    private synchronized void sellTicket() {
        if (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + "正在出售第" + (tickets--) + "张票 ");
        }
    }
}

输出:
窗口1正在出售第100张票
...
窗口1正在出售第95张票 
窗口3正在出售第95张票
 ...
窗口2正在出售第89张票
窗口1正在出售第88张票
窗口2正在出售第88张票 
...
窗口2正在出售第76张票 
窗口3正在出售第76张票
...
窗口2正在出售第1张票 
窗口3正在出售第1张票

输出结果看出线程不安全。说明同步方法锁对象不是 obj,而我们知道,每个类都有一个默认对象 this,所以同步方法的锁对象是 this。验证如下,将synchronized (obj)修改为synchronized (this)

public class SellTicket implements Runnable {
    ...

    @Override
    public void run() {
        while (true) {
            if (i%2 == 0) {
                synchronized (this) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + "正在出售第" + (tickets--) + "张票");
                    }
                }
            } else {
                sellTicket();
            }
            i++;
        }
    }

    private synchronized void sellTicket() {
        ...
    }
}

输出:
线程安全

故,同步方法的锁对象是 this。

静态同步方法

public class SellTicket implements Runnable {
    // 定义100张票
    private static int tickets = 100;
    
    @Override
    public void run() {
        while (true) {
            sellTicket();
        }
    }

    private static synchronized void sellTicket() {
        if (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + "正在出售第" + (tickets--) + "张票 ");
        }
    }
}

输出:
线程安全

因为静态方法随类的加载而加载,所以静态同步方法的锁对象是当前类的字节码对象。验证如下,将synchronized (obj)修改为 synchronized (SellTicket.class)

public class SellTicket implements Runnable {
    ...

    @Override
    public void run() {
        while (true) {
            if (i%2 == 0) {
                synchronized (SellTicket.class) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + "正在出售第" + (tickets--) + "张票");
                    }
                }
            } else {
                sellTicket();
            }
            i++;
        }
    }

    private static synchronized void sellTicket() {
        ...
    }
}

输出:
线程安全

线程同步的优缺点:

  • 好处
    使用同一个锁对象解决了多线程的安全问题。
  • 缺点
    当线程相当多时,因为每个线程都会去判断同步锁,这是很耗费资源的,无形中会降低程序的运行效率。

synchronized 存在的问题:申请资源的时候,如果申请不到,线程直接进入阻塞状态,而线程进入阻塞状态,啥都干不了,也不释放线程已经占有的资源! Java提供了Lock解决该问题。


线程同步在集合应用

Java提供了许多线程安全的类:StringBufferVectorHashtable等,通过查看源码发现,线程安全的类方法均通过synchronized修饰

但如果我们需要使用线程不安全的集合怎么办呢?

Java提供Collections.synchronizedList()可以将线程不安全的集合转换成线程安全

  • public static <T> List<T> synchronizedList(List<T> list)
public class ThreadDemo {
    public static void main(String[] args) {
        // 线程不安全
        List<String> list1 = new ArrayList<String>();

        // 线程安全
        List<String> list2 = Collections.synchronizedList(new ArrayList<String>());
        list2.add("1111");
        list2.add("2222");
    }
}

使用 Collections.synchronizedList(传入需要转换的集合对象),返回 List 对象。

看下 Collections.synchronizedList 源码:

SynchronizedList 就是在 List 的所有操作外加了一层 synchronized 同步代码块控制。

注:遍历线程安全集合时,还需要用户自己控制同步

源码:

查看源码发现,遍历迭代器并没有加 synchronize 修饰

测试:

public class SynchronizedList {
    public static void main(String[] args){
        List<String> list = Collections.synchronizedList(new ArrayList<String>());
        list.add("1111");
        list.add("2222");
        list.add("3333");

        Thread t1 = new Thread(new IteratorRunnable(list), "线程1");
        Thread t2 = new Thread(new ModifySynchronizeRunnable(list), "线程2");

        t1.start();
        t2.start();
    }
}

// 读取线程
class IteratorRunnable implements Runnable{
    private List<String> list;

    public IteratorRunnable(List<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        for (String s : list) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(s + ",");
        }
    }
}

// 写入线程
class ModifySynchronizeRunnable implements Runnable{
    private List<String> list;

    public ModifySynchronizeRunnable(List<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add("modify");
        }
    }
}

1111,
Exception in thread "线程1" java.util.ConcurrentModificationException
 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
 at java.util.ArrayList$Itr.next(ArrayList.java:859)
 at 多线程.IteratorRunnable.run(SynchronizedList.java:35)
 at java.lang.Thread.run(Thread.java:748)

异步修改 List 的结构,发现抛出了ConcurrentModificationException异常。

读取线程 IteratorRunnable 加入同步:

// 读取线程
class IteratorRunnable implements Runnable{
    private List<String> list;

    public IteratorRunnable(List<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        synchronized (list) {
            for (String s : list) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(s + ",");
            }
        }
    }
}

输出:
1111,
2222,
3333,

线程安全。


Lock锁

Lock是 Java 提供的另一种实现互斥锁的方式,实现为ReentrantLockReadWriteLock,基于AQS实现,并且解决了 synchronized 关键字的资源不可抢占问题。具体提供三种方式:

  1. 超时机制:指定时间内获取不到锁,不进入阻塞状态。
  2. 响应中断:给阻塞的线程发送中断信号的时候,能够唤醒它。
  3. 非阻塞的获取锁:当尝试获取锁失败,并不进入阻塞状态,而是直接返回。

对应Lock中的方法:

  1. boolean tryLock(long time, TimeUnit unit):超时
  2. void lockInterruptibly():响应中断
  3. boolean tryLock():非阻塞获取锁
  4. void lock():获取锁
  5. void unlock():释放锁
public class SellTicket implements Runnable {
    // 定义100张票
    private static int tickets = 100;

    private Lock lock = new ReentrantLock();

    int i = 0;

    @Override
    public void run() {
        while (true) {
            if (i%2 == 0) {
                lock.lock();
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + "正在出售第" + (tickets--) + "张票");
                    }
                lock.unlock();
            } else {
                lock.lock();
                sellTicket();
                lock.unlock();
            }
            i++;
        }
    }

    private void sellTicket() {
        if (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + "正在出售第" + (tickets--) + "张票 ");
        }
    }
}

输出:
线程安全
  • 队列同步器(AbstractQueueSynchronizer,AQS)

死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,这些永远在互相等待的进程称为死锁进程。

定义两个线程,通过标志flag判断进入不同的逻辑。
线程1:获取锁对象objA,失眠100ms,尝试获取objB。
线程2:获取锁对象objB,失眠100ms,尝试获取objA。

锁对象类MyLockpublic class MyLock {
    // 创建两把锁对象
    public static final Object objA = new Object();
    public static final Object objB = new Object();
}

死锁处理类DieLockpublic class DieLock extends Thread {

    private boolean flag;

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

    @Override
    public void run() {
        if (flag) {
            synchronized (MyLock.objA) {
                System.out.println("if objA");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (MyLock.objB) {
                    System.out.println("if objB");
                }
            }
        } else {
            synchronized (MyLock.objB) {
                System.out.println("else objB");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (MyLock.objA) {
                    System.out.println("else objA");
                }
            }
        }
    }
}

主控:
public class DieLockDemo {
    public static void main(String[] args) {
        DieLock dl1 = new DieLock(true);
        DieLock dl2 = new DieLock(false);

        dl1.start();
        dl2.start();
    }
}

输出:
if objA
else objB

输出一直等待,因为线程dl1、dl2相互等待形成死锁。

注:代码应避免死锁的出现。

死锁解决方法

要避免死锁就需要分析死锁发生的条件,只有以下这四个条件都发生时才会出现死锁:

  1. 互斥,共享资源 X 和 Y 只能被一个线程占用;

  2. 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
    解决办法:同时申请所有资源

  3. 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
    解决办法:使用Lock API

  4. 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
    解决办法:设置加锁顺序 例如:全都按照从小到大的顺序


锁分类

先引出问题:

多线程竞争锁时,阻塞等待锁释放是如何执行的?内部是用while死循环一直去判断锁是否被释放吗?

首先,锁分为两种,一种是互斥锁;一种是自旋锁

互斥锁一般应用于内核,当你的线程获取锁失败后,操作系统会将你的线程休眠(阻塞等待),同时把你的线程 id 记录到和这个锁相关的队列。当这个锁释放后,收到中断通知的线程会解除阻塞状态,加入到运行队列,等待新的 CPU 时间片的分配。

这里会有线程切换 Context Switch 的代价,即你的线程需要进入睡眠,然后再唤醒,这个是有消耗的,一般是几个us级别代价。

自旋锁:你的线程一直在尝试获得锁,如果之前获得锁的另外线程没有释放,你的线程就会一个死循环不停尝试获取。这时,循环尝试会浪费大量的 CPU 资源,你可以观测到这个cpu core的利用率是几乎100%。

如果其他线程很快就释放了锁,获取锁线程只尝试几次就获得了锁,那这个代价就会比上下文切换 Context Switch 小。可能只有是几十个ns。

一般临界区很短时,就优先使用自旋锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会叫的狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值