🧡💛💚💙💜🤎💗🧡💛💚💙💜🤎💗
感谢各位一直以来的支持和鼓励 , 制作不易
🙏 求点赞 👍 ➕ 收藏 ⭐ ➕ 关注✅
一键三连走起 ! ! !
🧡💛💚💙💜🤎💗 🧡💛💚💙💜🤎💗
【友情链接】➡➡▶线程入门学习,线程(多线程)的实现
【友情链接】➡➡▶线程入门学习,线程池 & Lambda表达式
一、线程安全的概述
如果有多个线程在同时运行,并且这些线程可能会同时运行一段相同的代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,这样就是线程安全的。
二、线程同步机制
当我们使用多个线程访问同一个资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。 解决多钱程并发访问一个资源的安全性问题,Java中提供了同步机制(synchronized)来解决。
Java中有三种方法实现同步操作:同步代码块,同步方法,锁机制。
案例,使用多线程模拟电影院卖票,卖100张票,有三个售票窗口
//通过实现Runnable接口来实现多线程
public class RunnableImp implements Runnable {
//设置总票数,100张票
private int ticket=100;
//重写run方法,设置线程任务,卖票
@Override
public void run() {
//死循环,重复卖票
while (true){
//查看票有没有卖完
if (ticket>0){
//没卖完,继续卖票
System.out.println(Thread.currentThread().getName()+"--正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
//模拟卖票
public class test {
public static void main(String[] args) {
RunnableImp runnableImp = new RunnableImp();
Thread thread = new Thread(runnableImp);
thread.start();//1号窗口
new Thread(runnableImp).start();//2号窗口
new Thread(runnableImp).start();//3号窗口
}
}
测试结果:
会线程安全问题1:售出相同的票
会线程安全问题2:售出不存在的票
1、同步代码块
使用格式:
synchronize(锁对象){
//可能出现线程安全的代码块
}
注意:
1。锁对象可以是任意的数据类型
2。多个线程对象要使用同一把锁
锁对象的作用:
把同步代码块锁住,只让一个线程在同步代码块中执行
示例代码;
//通过实现Runnable接口来实现多线程
public class RunnableImp implements Runnable {
//设置总票数,100张票
private int ticket=100;
//创建一个对象
Object obj=new Object();
//重写run方法,设置线程任务,卖票
@Override
public void run() {
//死循环,重复卖票
while (true){
//设置同步代码块
synchronized (obj){
//查看票有没有卖完
if (ticket>0){
//没卖完,继续卖票
System.out.println(Thread.currentThread().getName()+"--正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
}
运行test测试类,得到测试结果:
由测试结果,可以发现解决了卖出重复票和不存在的票的线程安全问题。
同步代码块实现同步技术的原理分析:
同步代码块中使用了一个锁对象也叫做(“同步锁”、“对象锁”、“对象监视器”)在多线程实现售票的案例中,有三个线程一起抢夺CPU的执行权,哪个线程抢夺到了CPU执行权,哪个就执行run()方法,当遇到了同步代码块synchronized时,就会检查代码块中是否有锁对象,如果有那么就会获取到该对象,并进入代码块中执行;那么剩下的线程,若是也有抢到了CPU的执行权,也会执行synchronized同步代码块并且判断是否有锁对象,若发现没有,那么此线程就会进入到阻塞状态,直到获取了锁对象的线程归还锁对象,也就是一直等到上一个线程执行完同步代码块中的代码并把锁对象归还到同步代码块中,此线程才能进入到同步中执行代码。
所以同步技术的核心就是,同步中的线程没有执行完毕不会释放锁对象;而同步外的线程没有锁对象则进不去同步
2、同步方法
解决线程安全问题的二种方案:使用同步方法
作用:
同步方法会将方法内部的代码锁住,只让一个线程使用。
使用步骤:
1.将访问了共享数据的代码也就是可能出现线程安全问题的代码抽取出来,
放到一个方法中去
2.给方法添加 synchronized 修饰狩
使用格式:
权限修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码块 )
}
示例代码:
//通过实现Runnable接口来实现多线程
public class RunnableImp implements Runnable {
//设置总票数,100张票
private int ticket=100;
//重写run方法,设置线程任务,卖票
@Override
public void run() {
//死循环,重复卖票
while (true){
sellticket();
}
}
//使用同步方法
public synchronized void sellticket(){
//查看票有没有卖完
if (ticket>0){
//没卖完,继续卖票
System.out.println(Thread.currentThread().getName()+"--正在卖第"+ticket+"张票");
ticket--;
}
}
}
运行test测试代码:测试结果和同步代码块测试结果一样,没有出现重复票或不存在的票
3、锁机制
java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更加广泛的锁定操作, 而且同步代码块和同步方法具有的功能Lock也都会有,并且Lock更能体现出面向对象。
Lock锁也称为同步锁,它的加锁和释放锁都已经方法化了:
public void lock(): 加同步锁。
public void unlock(: 释放同步锁.
示例代码:
解决线程安全问题的二种方案:使用Lock锁
使用步骤:
1.在成员位置创建一个ReentrantLock对象
2.在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁
3.在可能会出现安全问题的代码后调用Lock接口中的方法unLock释放锁
//通过实现Runnable接口来实现多线程
public class RunnableImp implements Runnable {
//设置总票数,100张票
private int ticket=100;
//1.在成员位置创建一个ReentrantLock对象
Lock lock = new ReentrantLock();
//重写run方法,设置线程任务,卖票
@Override
public void run() {
//死循环,重复卖票
while (true){
//2.在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁
lock.lock();
//查看票有没有卖完
if (ticket>0){
//没卖完,继续卖票
System.out.println(Thread.currentThread().getName()+"--正在卖第"+ticket+"张票");
ticket--;
}
//3.在可能会出现安全问题的代码后调用Lock接口中的方法unLock释放锁
lock.unlock();
}
}
}
运行test测试代码:测试结果和同步代码块测试结果一样,没有出现重复票或不存在的票
三、线程的六种状态
-
New(新建状态)
- 线程刚被创建时的状态,但是线程还未启动,还没调用start方法。
-
Runnable(可运行状态)
- 线程可以在java虚拟机中运行的状态,线程可能正在运行自己代码也可能没有,取决于操作系统的处理器。
- Blocked(锁阻塞状态)
- 当一个线程试图获取一个对象锁时,但是该对象锁已经被其他的线程持有,那么该线程就会进入Blocked阻塞状态;而当该线程持有锁对象时,该线程就会转变成Runnable可运行状态。
- Waiting(无限等待状态)
- 一个钱程在等待另一个线程执行一个(唤醒)动作时,那么该线程就是进入Waiting无限等待状态。进入这个状态后的线程是不能被自动唤醒的,必须要等待另外一个线程调用notify()或者notifyAll()方法才能够被唤醍。
- Timed Waiting(计时等待状态)
- 与waiting无限等待状态很相似,当调用带有超时参数的方法时,他们就会进入Timed Waiting计时等待状态。而该状态将会一直保持着,直到超时期满或者接收到唤醒通知。带有超时参数的常用方法有
Thread.sleep() 和 Object.wait()
。
- 与waiting无限等待状态很相似,当调用带有超时参数的方法时,他们就会进入Timed Waiting计时等待状态。而该状态将会一直保持着,直到超时期满或者接收到唤醒通知。带有超时参数的常用方法有
- Terminated(死亡状态)
- 已经退出的线程处于这种状态。
四、线程等待与唤醒机制
等待唤醒中使用的方法
等待唤醒机制用于解决线程间通信的问题,
使用的3个方法如下:
- 1.wait()方法 :调用了wait()方法候,线程就不再参与调度,进入 wait set中,不会浪费CPU资源,也不会竞争锁。这种线程状态就是WAITING(等待状态)。同时它还会等着别的线程执行特别的动作—>通知( notify) 在这个对象上等待的线程就会从walt set中释放出来,重新进入到调度队列 ( ready queue)中
- 2.notify()方法 :选择所通知对象的 wait set 中的一个线程并释放它。也就是唤醒在此监视器上等待的单个线程
- 3.notifyAll():释放所通知对象的wait set 中的全部线程。也就是唤醒在此监视器上等待的所有线程
注意:被通知的线程是不能立即进入到执行状态的,因为此前该线程实在同步块内中断的,此时该线程已经没有锁对象了,所以他需要再次的获取锁对象,那么此时就可能会有其他的线程与其竞争锁对象,该线程竞争成功i获得锁对象后才会在之前调用到wait()方法之处恢复执行。
所以可以总结到:若线程能够获取到锁,那么线程就会从等待状态转变成运行状态;否则线程则只是从wait set中出来又进入到entry set中,线程就会又从等待状态转换到阻塞状态
调用wait()和notify()方法需要注意的细节
- 1.wait方法与notify方法必须要由同一个锁对象调用。因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait()方法后的线程。
- 2.wait()方法与notify()方法都是Object类的方法。因为锁对象可以是任意类型的对象,而任意类的对象所属的类都是继承了Object类的。 I
- 3.walt()方法与notlfy()方法必须要在同步代码块或者是同步方法中使用。因为要调用这两个方法,必须要通过锁对象。
案例:客户去牛肉面馆吃牛肉面,面馆老板首先是在等待客户点面,等到了一个客户点了一碗牛肉面,老板开始做面,客户则进入等待,等到老板面做好了就会告知客户面好了,客户开始吃面。
示例代码:
//创建牛肉面类,
class BeefNoodles{
//面汤种类:金汤,红汤,
String tang;
//口味:不辣、一般辣
String kouwei;
//面的状态,有面true, 没面false
boolean flag =false;
}
//创建牛肉面馆类线程
public class BeefNoodlesRestaurant extends Thread {
//创建一个牛肉面类的,牛肉面变量
private BeefNoodles beefNoodles;
//使用有参构造方法给牛肉面变量赋值
public BeefNoodlesRestaurant(BeefNoodles beefnoodles) {
this.beefNoodles = beefnoodles;
}
//重写run方法,设置线程任务:做牛肉面
@Override
public void run() {
//定义变量,决定制作的牛肉面的口味,
int count = 2;//默认金汤不辣
while (true){//死循环,一直制作牛肉面
//使用同步代码块来保护线程安全
synchronized (beefNoodles){
//判断牛肉面的状态,决定是否进入线程等待状态
if (beefNoodles.flag==true){
//牛肉面已经存在(已做好),beefNoodles调用wait()方法进入等待
try{
beefNoodles.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//唤醒线程,制作牛肉面
//设置牛肉面的口味
if (count%2==0){
beefNoodles.tang="金汤";
beefNoodles.kouwei="不辣";
}else if (count%3==0){
beefNoodles.tang="红汤";
beefNoodles.kouwei="不辣";
}else if (count%5==0){
beefNoodles.tang="金汤";
beefNoodles.kouwei="一般辣";
}else if (count%7==0){
beefNoodles.tang="红汤";
beefNoodles.kouwei="一般辣";
}
//确认牛肉面的种类
System.out.println("正在做"+beefNoodles.tang+beefNoodles.kouwei+"牛肉面");
//制作牛肉面需要的时间6秒
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//牛肉面馆制作好了牛肉面
beefNoodles.flag=true;//设置面的状态
count++;
//唤醒客户吃面
beefNoodles.notify();
System.out.println("客观的"+beefNoodles.tang+beefNoodles.kouwei+"牛肉面已经制作好了");
}
}
}
}
//创建客户类线程
public class KeHu extends Thread {
//创建一个牛肉面类的,牛肉面变量
private BeefNoodles beefNoodles;
//使用有参构造方法给牛肉面变量赋值
public KeHu(BeefNoodles beefnoodles) {
this.beefNoodles = beefnoodles;
}
//重写run方法,设置线程任务:吃牛肉面
@Override
public void run() {
int i=1;//客户序号
while (true){//死循环,一直吃牛肉面
//使用同步代码块来保护线程安全
synchronized (beefNoodles){
//判断牛肉面的状态,决定是否进入线程等待状态
if (beefNoodles.flag==false){
//牛肉面不存在(没有做好),beefNoodles调用wait()方法进入等待
try {
beefNoodles.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//线程唤醒之后执行的状态
System.out.println("客户"+Thread.currentThread().getName()+"-->"+i+"正在吃"
+beefNoodles.tang+beefNoodles.kouwei+"的牛肉面");
//客户吃完牛肉面,修改面的状态
beefNoodles.flag =false;
//面吃完了,唤醒面馆制作面
beefNoodles.notify();
System.out.println("客户"+Thread.currentThread().getName()+"-->"+i+"已经吃完了"
+beefNoodles.tang+beefNoodles.kouwei+"的牛肉面"+",面馆开始继续制作牛肉面");
i++;//下一位客户
System.out.println("----------------------------------");
}
}
}
}
//测试类
public class Test {
public static void main(String[] args) {
//创建一个牛肉面的对象
BeefNoodles beefNoodles = new BeefNoodles();
//开启牛肉面馆线程,制作面条
new BeefNoodlesRestaurant(beefNoodles).start();
//开启客户线程,客户开始吃面条
new KeHu(beefNoodles).start();
}
}
测试结果: