活动地址:CSDN21天学习挑战赛
文章目录
一.线程的安全问题
1.1 需求
假设现在有个电影院,播放一部很欢迎的电影
《阿伟的故事》,那么我们该怎么去售票呢?
开一个窗口?,这样会不会卖的太慢了哇,那要是开多个窗口,就涉及到了数据交互的问题了,我们该怎么去解决?
1.1.1 第一种解决办法:
用一个窗口售票,这样一定是没有问题的
1.1.2 第二种解决办法
多窗口,但是是有序的,每个窗口售卖固定且分配好的的票数。
注意:
因为没有出现数据共享,所以这种情况也是没有任何问题的
1.1.3 第三种解决办法,实际情况
存在的问题
:
1.出售重复的票
2.出售不存在票:0
需求分析
:
1.每一个窗口就是一个线程 使用线程来表示多窗口售票
2.只有出现数据共享的时候才会产生问题。
3.使用Runnable(或者callable)方式来创建线程,因为这两种方式可以创建多个线程
3.定义一个变量来表示票数
4.循环售票
线程创建类
public static class MyRunnable implements Runnable{
private int count =100;
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//使用循环一直买票
while (true){
if (count > 0){
if (count > 0){
System.out.println(Thread.currentThread().getName()+"\t"+"正在购买第"+count+"张票");
}
count -- ;
}
}
}
}
测试类
public static void main(String[] args) {
//实例化MyRunnable对象
MyRunnable runnable = new MyRunnable();
//实例化线程
Thread t1 = new Thread(runnable); t1.start();
//实例化线程
Thread t2 = new Thread(runnable); t2.start();
// 实例化线程
Thread t3 = new Thread(runnable); t3.start(); }
}
结果
注意
:结果黄黄在这里不予展示,自己测试的话多测试几次,最好多打开几个应用去测试。重复的票还算容易测试出来,0票非常难测试出来。
1.1.3.1 出现重复的票的原因
当线程一输出完这句话,或者正在输出这一句话的时候
这个时候线程二进来了,他也来输出这句话,即抢占资源
这时候,即线程一正在打印这句话,且还没有去执行
操作的时候,线程二进来了,也去打印。
故此,两者打印的票数相同。
1.1.3.2 出现0票的原因
同理,输出-1票的原因也很简单
1.当票数为1的时候,线程一抢到cup的执行权,执行打印语句
2.这时候线程二进来了,抢到了cpu的执行权,只是进入选择结构中,
3.线程一枪到了cpu执行权,执行count -- ;
4.线程二抢到cup的资源出售不存在的票
二.线程安全的解决办法
解决上述问题的根本:线程的操作只能让一个线程执行操作,一个线程执行完成操作之后,另一个线程才能执行操作。
2.1 解决办法一: synchronized锁对象
1.语法:
synchronized(锁对象){
可能产生问题的代码
}
2.注意点
A.锁的对象可以是任意的对象
B.所有线程锁的对象必须是同一个对象
.
3.作用:
解决数据共享安全的问题
线程定义类
public class two implements Runnable {
//定义一个变量来记录票数
private int count =100;
//定义一个锁对象,可以是任意对象
private Object object =new Object();
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//使用循环买票
while(true){
synchronized (object){
if (count>0){
System.out.println(Thread.currentThread().getName() + "\t" + "two您购买的是第" + count + "张票");
count --;
}
}
}
}
}
测试类
public static void main(String[] args) {
//实例化MyRunnable对象
two two = new two();
//实例化线程
Thread t1 = new Thread(two);
t1.start();
//实例化线程
Thread t2 = new Thread(two);
t2.start();
// 实例化线程
Thread t3 = new Thread(two);
t3.start();
}
结果
不难发现,上面的结果都是顺序输出的,也没有执行抢占,这是为什么呢
原因分析
总结:同步代码块,保证只有一个线程在此段时间内能够执行线程操作。
2.2 解决办法二:同步方法 (还是synchronize锁)
1.语法:
访问修饰符 synchronized 返回值类型 方法的名称(参数列表) {
方法体
retrun 返回值
}
2.说明:同步方法可以是普通的成员方法 也可以是静态的方法
3.说明:
A.成员方法的锁的对象就是this
B.静态方法锁的对象是当前类的class对象
线程实现类
public class MyRunnable implements Runnable {
private static int count = 100;
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true) {
synchronized (MyRunnable.class) {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "\t" + "您 购买是第" + count + "张票");
count--;
}
}
}
}
public static synchronized void showInfo() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "\t" + "您购买是 第" + count + "张票");
count--;
}
}
}
测试类
ublic static void main(String[] args) {
//实例化MyRunnable对象
MyRunnable two = new MyRunnable();
//实例化线程
Thread t1 = new Thread(two);
t1.start();
//实例化线程
Thread t2 = new Thread(two);
t2.start();
// 实例化线程
Thread t3 = new Thread(two);
t3.start();
}
测试结果:
依旧是有序的队列,且不发生抢占资源的情况。
2.3第三种解决办法:Lock锁
1.Lock 简介
A.Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
B.此实现允许更灵活的结构
2. 实现类ReentrantLock
3.方法
注意两个方法
方法一
:void lock() 获取锁
方法二
:void unlock() 释放锁
线程类
public class MyRunnable implements Runnable {
//定义一个变量来记录票数
private int count = 1000;
//实例化Lock锁对象
private Lock l = new ReentrantLock();
@Override
public void run() {
//获取锁对象
l.lock();
try {
Thread.sleep(500);
//使用循环一直买票
while (true) {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "\t" + "您购买是第" + count + "张 票");
count--;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁对象
l.unlock();
}
}
}
测试类
public static void main(String[] args) {
//实例化MyRunnable对象
MyRunnable two = new MyRunnable();
//实例化线程
Thread t1 = new Thread(two);
t1.start();
//实例化线程
Thread t2 = new Thread(two);
t2.start();
// 实例化线程
Thread t3 = new Thread(two);
t3.start();
}
结果
三.死锁:
3.1什么是死锁
1.死锁:
A占用B的锁对象 B占用A的锁对象 A与B互不谦让 A与B出现同时等待的状态
2.生活中:
卢伟亮与黄自强在一个宿舍 宿舍中只有一双拖鞋 两人都想出去买东西 卢伟亮霸占左拖鞋 黄自强 霸占右拖鞋 互补谦让 都出不去
3.2四个必要条件才能产生死锁
产生死锁的四个必要条件
(1)互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
(2)请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
(3)不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
(4)环路等待条件:是指进程发生死锁后,必然存在一个进程–资源之间的环形链
3.3解决死锁的方法
处理死锁的基本方法
1.预防死锁:通过设置一些限制条件,去破坏产生死锁的必要条件
2.避免死锁:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁
3.检测死锁:允许死锁的发生,但是通过系统的检测之后,采取一些措施,将死锁清除掉
4.解除死锁:该方法与检测死锁配合使用
四.速记
4.1Runnable 和 Callable 的 区别?
=相同点==
1)两者都是接口
2)两者都可以用来编写多线程代码
3)两者都可以调用Thread.start()来启动线程
不同点==
1)最大的区别,Runnable没有返回值,而实现Callable接口的任务线程能返回执行结果
2)Callable接口实现类中的run方法允许异常向上抛出,可以在内部处理,try catch,但是Runnable
接口实现类中run方法的异常必须在内部处理,不能抛出
3)Runnable可以作为Thread构造器的参数,通过开启新的线程池来执行,也可以通过线程池来执行;
而Callable只能通过线程池来执行。
4.2 synchronized 和 volatile 的区别?
1)volatile是变量修饰符,而synchronized则可以修饰代码块或方法。
2)volatile不需要加锁,比synchronized更轻量级,不会阻塞线程;而synchronized加锁,会导致线程
阻塞(互斥性)
3)synchronized既能够保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
4)synchronized会执行编译优化(对字节码重新排序),编译优化有可能导致运行过程中异常。而
volatile禁止字节码重新排序,不会引发编译优化导致的异常。
原子性:一个操作不能被打断,要么全部执行完毕,要么不执行
可见性:一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量这种修改(变
化)。
并发编程三大特征:原子性。可见性,有序性
volatile作用:修饰变量,这个变量就会满足可见性与有序性,当线程修改这个变量的时候,别的线程
就会感知到这个变量进行了修改,
4.3 synchronized 和 Lock 的区别?
1)语法不同:
synchronized 是Java的关键字或修饰符,在jvm层面上,修饰方法或代码块。
Lock不是修饰符,是一个接口(加锁的工具类,该类提供很多方法加锁、释放锁等)tryLock()
unLock()
2)释放锁不同():
synchronized 获取锁的线程执行完同步代码,释放锁,且线程执行发生异常,jvm会自动让线程释
放锁(不管成功还是失败,都会自动释放锁)
Lock必须手动在finally中释放锁(必须手动释放锁)
3)死锁情况不同():
synchronized 在发生异常时候会自动释放占有的锁,因此不会出现死锁;
Lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生
4)锁判断不同:
synchronized 无法判断当前线程的上锁状态
Lock可以判断当前线程的上锁状态 tryLock unLock isLock
5)锁类型不同:
synchronized是可重入 不可判断 非公平
Lock:是可重入 可判断 可公平
公平性(线程等待时间长,优先获取锁)