目录
线程安全
多线程产生的问题
图解
代码实现:
线程类:
public class RunnableImpl implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true)
if (ticket>0) {
System.out.println(Thread.currentThread().getName() + "正在买第" + ticket + "张票");
ticket--;
}
}
测试方法
public static void main(String[] args) {
RunnableImpl runnable=new RunnableImpl();
Thread thread0=new Thread(runnable);
Thread thread1=new Thread(runnable);
Thread thread2=new Thread(runnable);
thread0.start();
thread1.start();
thread2.start();
}
效果:
不同的线程卖出了相同的票
该线程安全问题的解释:
3个线程一起抢夺cpu执行权,同时在执行打印第100张票,这时ticket还没执行到ticket--
特别是当if后面设置了sleep后,线程同时进入睡眠,例如线程1打印第1张票前进入睡眠,ticket还未--,线程2、3也进入打印第1张票的睡眠,线程1先醒将票1打印且--,那么当线程2、3醒的时候就会打印票-1、-2
注意:
线程安全问题是不能产生的,我们可以让一个线程在访问共享数据的时候,无论是否失去了cpu的执行权让其他的线程只能等待,等待当前线程卖完票,其他线程在进行卖票
保证:始终一个线程在买票
线程同步
1、同步代码块
2、同步方法
3、锁机制
同步代码块:synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
注意:
1、通过代码块中的锁对象,可以使用任意的对象
2、但是必须保证多个线程使用的锁对象是同一个
3、锁对象作用:
把同步代码块锁住,只让一个线程在同步代码块中执行
Object object=new Object();
@Override
public void run() {
while (true)
synchronized (object){
if (ticket>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在买第" + ticket-- + "张票");
}
}
同步技术原理:先抢到cpu执行权的线程会拿走锁对象,第二线程个执行到synchronized代码块的线程会等待第一个线程执行完毕归还锁对象(进入阻塞状态),再执行synchronized代码块
保证了只能有一个线程在同步中执行共享数据,保证了安全
程序的频繁判断、获取锁、释放锁,程序的效率会降低
同步方法:使用synchronized修饰的方法,叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
卖票演示:
@Override
public void run() {
while (true){
//调用同步方法
payTicket();
}
}
//定义同步方法
public synchronized void payTicket() {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在买第" + ticket-- + "张票");
}
}
原理:
同步方法也会把方法内部的代码锁住
只让一个线程执行
同步方法的锁对象是谁?
就是实现类对象 new RunnableImpl()
也就是this
实验:
通过在main方法中打印Runnable对象和run方法中打印this得出
他们的对象是一样的,所有同步方法锁住的对象就是Runnable的实现类对象
所以同步方法=如下写法
synchronized (this){if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在买第" + ticket-- + "张票");
}
静态同步方法:
注意要将使用的变量声明为静态的
public static synchronized void payTicketStatic(){
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在买第" + ticket-- + "张票");
}
}
探究锁对象是谁
这时候就不能是this了
this是对象创建之后的,静态方法优先于对象
静态方法的锁对象是本类的class属性-->class文件对象(反射)
lock锁:
jdk1.5之后出现在java.util.concurrent.locks.lock下
提供了比synchronized代码块和synchronized方法更加广泛的锁操作
同步代码块/同步方法具有的功能Lock都有,除此之外更加面对对象
lock接口中的方法:
lock()获取锁
unlock()释放锁
使用步骤:
1、在成员位置创建一个lock的实现类叫ReentrantLock对象
2、在可能出现安全问题前的代码调用Lock接口中的方法lock获取锁
3、在安全代码后使用unlock释放锁
演示:
private int ticket=100;
Lock lock=new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock();
if (ticket>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + "正在买第" + ticket-- + "张票");
}
}
}
效果:
线程状态
概述:线程被创建之后的生命周期内有多种状态
线程状态 | 导致状态发生的条件 |
NEW(新建) | 线程刚被创建未调用start |
Runnable(可运行) | 线程可以在虚拟机中运行的状态,可能正在运行自己的代码也可能没有,这取决于操作系统处理器 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而对象锁被其他线程持有则进入Blocked,若持有对象锁则进入Runnable |
Waiting(无限等待) | 一个线程在等待另一个线程执行(唤醒)动作时,该线程进入Waiting,该状态不能自动唤醒,必须等待另一个线程调用notify方法或者notifyAll方法 |
TimedWaiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们进入TimedWaiting状态,这一状态一直到超时期满或被唤醒,常用方法有sleep |
Terminated(死亡状态) | 因为run方法正常退出或遇到未处理的异常,终止了run方法而死亡 |
注意:1、wait、notify和notifyAll都是Object类的方法,索引锁对象可以是任意对象
2、且notify通知后并不是立马恢复执行,需要等待再次获得锁
3、wait和notify需要由同一个锁对象调用,因为对应的锁对象可以通过notify来唤醒同一个锁下的wait线程
4、wait与notify必须要在同步代码块或是同步函数中使用,因为:必须要通过锁对象来调用这两个方法
等待唤醒案例
/*
等待唤醒案例:线程之间的通信
需求老板卖包子,顾客来买包子
顾客告知老板包子种类和数量,调用wait方法,放弃cpu执行进入waiting
老板线程花5s做包子,做好后调用notify唤醒顾客吃包子
注意:
顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
同步使用的锁对象必须保证唯一
只有锁对象才能调用wait和notify
*/
public class DemoWaitAndNotify {
public static void main(String[] args) {
//锁对象
Object obj=new Object();
//顾客线程
new Thread(){
@Override
public void run() {
while (true){
//同步代码块保证等待和唤醒的线程只有一个
synchronized (obj){
System.out.println("告知老板包子的种类和数量");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后的代码
System.out.println("包子已经做好了,开食");
System.out.println("-------------------");
}
}
}
}.start();
//老板线程
new Thread(){
@Override
public void run() {
while (true){
try {
Thread.sleep(5000);//5s做包子
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
System.out.println("5s后告知顾客吃包子");
obj.notify();
}
}
}
}.start();
}
}
由这个案例学到的东西:synchronized代码的锁对象是保证同一时间只能有一个线程执行,当先执行的线程调用obj.wait之后该线程就进入了停止状态,老板线程就可以进行工作了老板线程再调用obj.notify后唤醒顾客线程,让顾客吃包子
带参的wait和notifyAll
进入到timeWaiting(计时等待)有两种方式
1、使用sleep方法,在毫秒值结束后,线程睡醒进入到Runnable/Block状态
2、使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没被notify唤醒,就会自动醒
带参wait案例(上回的代码,删除了老板线程):
//锁对象
Object obj=new Object();
//顾客线程
new Thread(){
@Override
public void run() {
while (true){
//同步代码块保证等待和唤醒的线程只有一个
synchronized (obj){
System.out.println("告知老板包子的种类和数量");
try {
obj.wait(5000); 5s后自动唤醒吃包子
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后的代码
System.out.println("包子已经做好了,开食");
System.out.println("-------------------");
}
}
}
}.start();
notifyAll()唤醒在此对象监视器上等待的所有线程
案例:
这次有两个顾客,老板还是上回的代码
使用notify方法唤醒
效果:每次只有一个顾客开食
换成notifyAll后
顾客1和顾客2同时开食
总结:notify是有多个线程时随机唤醒一个线程
notifyAll是唤醒全部