概念
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象时线程安全的。
该定义要求线程安全的代码都必须具备一个共同特征:
代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程下的调用问题,更无须自己实现任何措施来保证多线程环境下的正确调用。
模拟简单的卖票程序
public class Ticket implements Runnable{
private int ticket = 10;
@Override
public void run() {
//获取当前程序执行所属的线程的名称
String threadName = Thread.currentThread().getName();
while(true){
if(ticket>0){
System.out.println(threadName+"正在售卖第"+ticket+"张票");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}else{
break;
}
}
}
}
测试
package test7;
/**
* 多线程执行相同的程序会出现线程安全的问题(共享的变量两个线程都进行作可能出现错误情况)
*
* 以后在开发的时候,如果多线程实现一个功能,有交量值修改的作一定要注意线程的同步问题
*/
public class ThreadTest01 {
public static void main(String[] args) {
//实例化卖票美的对象
Ticket ticket = new Ticket();
//把卖票对象作为参数创建线程,启动线程开始卖票(创建了小明这个线程,用来执行卖票程序)
Thread t1=new Thread(ticket,"小明");
Thread t2=new Thread(ticket,"小兰");
t1.start();
t2.start();
}
}
线程同步
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决
同步代码块
package test7;
public class Ticket implements Runnable {
//自己资源变量,模拟票的总数
private int ticket = 10;
//定义一个同步资源锁
Object lock = new Object();
@Override
public void run() {
//1.获取当前程宁行所属的线程的名称
String threadName = Thread.currentThread().getName();
while (true) {
//同步代码块:把大括号中的代码,使用资源锁同步起来(Cpu在执行该线程程序的时候,这段代码比完执行完才能离开)
synchronized (lock) {
if (ticket > 0) {
//卖第ticket张票
System.out.println(threadName + "正在售卖第" + ticket + "张票");
//模拟打印出票环节,系统等待一会
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卖完了,总数要减1
ticket--;
} else {
break;//投票结束循环
}
}
}
}
}
测试结果
同步方法
package test7;
public class Ticket1 implements Runnable {
//自己资源变量,模拟票的总数
private int ticket = 10;
@Override
public void run() {
//1.获取当前程宁行所属的线程的名称
String threadName = Thread.currentThread().getName();
//2.一直卖票
while (true) {
boolean flag=fun(threadName);
if (flag){
break;
}
}
}
//同步方法
private synchronized boolean fun(String threadName) {
if (ticket > 0) {
//卖第ticket张票
System.out.println(threadName + "正在售卖第" + ticket + "张票");
//模拟打印出票环节,系统等待一会
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卖完了,总数要减1
ticket--;
return false;
} else {
return true;
}
}
}
同步资源锁(Lock)
public class Ticket2 implements Runnable{
//自己资源变量,模拟票的总数
private int ticket = 10;
//定义一个同步资源锁
Lock lock = new ReentrantLock();
@Override
public void run() {
//1.获取当前程序执行所属的线程的名称
String threadName = Thread.currentThread().getName();
//2.一直卖票
while(true){
lock.lock();
//资源的操作--卖票
if(ticket>0){
//卖第ticket张票
System.out.println(threadName+"正在售卖第"+ticket+"张票");
try {
//模拟打印出票环节,系统等待一会
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卖完了,总数要减1
ticket--;
}else{
//没票结束循环
break;
}
//操作完毕解锁
lock.unlock();
}
}
}
注意事项:
Lock() 一定要放在try外面
- 如果放在try里面,如果try里面出现异常,还没有加锁成功就执行finally里面的释放锁的代码,就会出现异常
- 如果放在try里面,如果没有锁的情况下释放锁,这个时候产生的异常就会把业务代码里面的异常给吞噬掉,增加代码调试的难度
两种锁区别
synchronized和lock的区别
- 关键字不同
- synchronized自动进行加锁和释放锁,而Lock需要手动加锁和释放锁
- synchronized是JVM层面上的实现,而Lock是Java层面锁的实现
- 修饰范围不同,synchronized可以修饰代码块,静态方法,实例方法,而Lock只能修饰代码块
- synchronized锁的模式是非公平锁,而lock锁的模式是公平锁和非公平锁
- Lock的灵活性更高
线程通讯
所谓的线程通讯就是在一个线程中的操作可以影响另一个线程,wait(休眠线程),notify(唤醒一个线程),notifyall(唤醒所有线程)
wait和notify
多线程的调度过程是充满随机性的,系统源码是改变不了的,所以系统层面上不能解决问题。
但是在实际开发中我们希望合理的协调多线程之间的先后顺序。
通过 wait和notify 机制,来对多线程之间的执行顺序,做出一定的控制。
当某个线程调用 wait 之后,就会阻塞等待。
直到其他某个线程调用 notify 把这个线程唤醒为止。
注意事项:
- 将lock.notify()修改为lock.notifyAll(),则三个线程都能被唤醒
- wait在不传递任何参数的情况下会进入waiting状态(参数为0也是waiting状态);当wait里面有一个大于0的整数时,它就会进入timed_waiting状态
生产者和消费者案例
资源类对象:多个线程共同操作的对象
package test8;
/**
* 资源类对象:多个线程共同操作的对象
*/
public class BaoZi {
String name;//包子名称
boolean flag;//包子的状态(true表示存在 false表示不存在)
}
对于吃货-----消费者: 如果包子 不存在 进入等待状态 如果包子 存在 进入执行状态,吃包子。 吃完之后包子变为不存在 此时唤醒早餐店制作包子
package test8;
public class ChiHuo extends Thread {
BaoZi baoZi;
//构造方法:用来指定线程的名字和要操作的资源
public ChiHuo(String name, BaoZi bz) {
super(name);
this.baoZi = bz;
}
/**
* 对于吃货-----消费者:
* 如果包子 不存在 进入等待状态
* 如果包子 存在 进入执行状态,吃包子。 吃完之后包子变为不存在 此时唤醒早餐店制作包子
*/
@Override
public void run() {
String threadName = Thread.currentThread().getName();
int count = 0;
while (true) {
synchronized (baoZi) {
count++;
if (count > 10) {
break;
}
if (baoZi.flag) {//如果包子存在
System.out.println(threadName + "开始吃" + baoZi.name);//吃包子
baoZi.flag = false; //修改状态
baoZi.notify(); //唤醒同一资源下的其他线程
} else {//如果包子不存在
try {
baoZi.wait();//进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
对于早餐店----生产者: 如果包子 不存在 进入执行状态,制作包子。 制作完毕包子存在,唤醒吃货 如果包子 存在 进入等待状态
package test8;
public class ZaoCanDian extends Thread {
BaoZi baoZi;
//构造方法:用来指定线程的名字和要操作的资源
public ZaoCanDian(String name, BaoZi bz) {
super(name);
this.baoZi = bz;
}
/**
* 对于早餐店----生产者:
* 如果包子 不存在 进入执行状态,制作包子。 制作完毕包子存在,唤醒吃货
* 如果包子 存在 进入等待状态
*/
@Override
public void run() {
//获取当前线程的名称
String threadName = Thread.currentThread().getName();
int count = 0;
while (true) {
synchronized (baoZi) {
count++;
if (count > 10) {
break;
}
if (baoZi.flag) { //如果包子存在
try {
baoZi.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else { //如果包子不存在
System.out.println(threadName + "开始制作" + baoZi.name); //制作包子
baoZi.flag = true; //更改包子状态
baoZi.notify(); //唤醒同一资源下的其他线程
}
}
}
}
}
测试
package test8;
public class Test01 {
public static void main(String[] args){
//定义资源对象
BaoZi baoZi=new BaoZi();
baoZi.name="韭菜鸡蛋";
baoZi.flag=true;
//定义两个线程,起名字且操作同一个对象
ChiHuo ch=new ChiHuo("猪八戒",baoZi);
ZaoCanDian zcd=new ZaoCanDian("春光早餐",baoZi);
//启动线程
zcd.start();
ch.start();
}
}
运行结果