【Java基础|第29篇】 多线程(二)——线程安全,线程同步,线程通信,线程池
线程安全、线程同步与线程通信摘要: 多线程并发访问共享变量时可能导致数据不一致,称为线程安全问题。该问题主要由全局变量和静态变量引起,当多个线程同时执行写操作时发生。解决方法包括线程同步技术,通过synchronized关键字实现锁机制,确保同一时间只有一个线程访问共享资源。同步方式分为同步代码块和同步方法,前者通过锁对象控制访问,后者将整个方法作为同步单元。线程通信通过wait()、notify
(八)多线程
10、线程安全:
为什么会出现线程安全的名词:
目前为了提高运行效率,多线程的使用越来越广泛;
但他也带来了许多问题,如果有多个线程,它们在一段时间内,并发访问堆区中的同一个变量(含写入操作),由于线程的速度很快,那么最终可能会出现数据和预期结果不符的情况,这种情况就是线程安全问题。
(1)概述:
线程安全问题都是由全局变量及静态变量引起的
若多个线程同时执行写操作,就很可能出现线程安全问题
(2)解决方法:
线程同步技术
11、线程同步:
(1)概述:
由关键字synchronized实现
(2)线程同步方式:
利用锁机制同步代码块,同步方法
(3)同步代码块:
<1>定义:
利用锁机制实现共享资源的互斥访问;
保证同步代码块中的操作不会被别的线程打断,要么不执行要么就全部执行完;
<2>工作原理:锁机制
①获取锁:进入同步代码块之情,必须先获得锁对象的使用权
②互斥访问:同一时间,只有一个线程可以持有锁,其他线程必须等待锁释放
③释放锁:线程执行完代码块之后,自动释放锁,其他等待的线程可以争夺锁了
<3>格式:
在方法中写synchronized(锁对象){
需要同步执行的操作;
}
锁对象可以是Object类或者还他的子类对象
这里我们对象的选取,一般是所有线程公有的对象。
<4>流程:
抢占到共享资源后(上锁成功),线程才进入代码块,这时其他抢占失败的线程会进入blocked状态;
同步代码块中的代码执行完毕或者遇到break,return,异常这种情况,都会自动释放锁,其他线程争抢到锁后会由阻塞转为运行中。
<5>注意:
要求所有的线程都参与线程同步,且锁对象一致时,才有用
同步代码块不会阻止cpu切换进程,只是只要这个线程的同步代码块没执行完,即使时间片分配给别的线程,别的线程也不能进入代码块,只能执行其他的。
(4)同步方法:
<1>定义:
将整个方法都作为同步代码块处理。
就是这个方法被调用了之后,只要没调用完,别的线程就不能调用
<2>定义格式:
普通方法:public synchronized void methodName(){
//方法体
}
相当于:
public void methodName(){
synchronized(this){
//方法体
}
}
抽象方法:public static synchronized void methodName(){
//方法体
}
相当于:
public void methodName(){
synchronized(xxx.class){
//方法体
}
}
对于静态方法,默认使用定义该方法的类的class对象。
<3>作用:
由于方法的锁是自己本身的对象,因此这是线程对同一个对象的竞争,而不是方法;如果线程操作不同对象,那同步方法互不干扰。
静态方法就是把对象换成类,只要是同一个类就要争夺。(不管是使用普通方法还是类调用静态,只要是这个类的,就要抢)。
class Printer {
//普通同步方法: 锁对象默认为this
public synchronized void print1() {
System.out.print("天");
System.out.print("天");
System.out.print("向");
System.out.print("上");
System.out.println();
}
public void print2() {
//同步代码块,也使用this作为锁对象
//测试时,可以注释同步代码块,或使用其他锁对象,然后观察程序运行效果
//synchronized (Printer.class) {
synchronized (this) {
System.out.print("努");
System.out.print("力");
System.out.print("学");
System.out.print("习");
System.out.println();
}
}
}
public class Test16_Funtion {
public static void main(String[] args) {
//准备一个对象
final Printer p = new Printer();
//创建子线程1,输出100次 "好好学习"
Thread th1 = new Thread() {
@Override
public void run() {
for(int i = 0; i < 100; i++)
p.print1();
}
};
//创建子线程2,输出100次 "天天向上"
Thread th2 = new Thread() {
@Override
public void run() {
for(int i = 0; i < 100; i++)
p.print2();
}
};
th1.start();
th2.start();
}
}
这样写保证了线程1抢到对象,调用方法时,线程2即使拿到时间片也无法使用对象,无法调用方法,这样就可以使“好好学习”这四个字永远是连在一起的。
12、线程通信:
出现通信线程的原因:
只有线程同步,我们仅能保证线程在执行的过程中不被打断,但是我们无法保证线程执行的顺序。
例如我们上面的“好好学习”后面我们不能保证是“好好学习”还是“天天向上”
(1)定义:
多个线程一起执行,在默认情况下,cpu是随机切换进程的
但是我们需要这些线程有顺序的协作完成任务,那么多线程之间就需要协调通信
(2)实现原理:等待唤醒机制
wait()与notify()、notifyAll()
这三个方法全都在Object类中的方法,必须由锁对象调用
只有在同步代码块或者方法中才可以使用
wait():是线程进入无限等待状态,等待锁对象调用notify将它唤醒
notify():选取随机一个线程唤醒
notifyAll():将所有执行了wait()方法的线程唤醒
即使被唤醒也要争夺锁对象,挣不到就阻塞。
状态由waiting变为runnable
调用wait与notify的必须是同一个锁对象。
案例展示:这是一个双线程的:
//包子类
class Bum {
//包子数量
int num = 0;
//包子存在标识
boolean flag = false;
}
//生产者
class Producer extends Thread {
private Bum bum;
public Producer(String name, Bum bum) {
super(name);
this.bum = bum;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
//同步
synchronized (bum){
//根据flag判断包子是否存在,如果存在则 线程进行等待
if(bum.flag){
try {
bum.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生产包子
System.out.println("第" + i + "次," + this.getName() + ": 开始生产包子...");
bum.num++;
System.out.println("生产完成,包子数量: " + bum.num + ",快来吃!");
//生产完成,修改flag存在标识为true
bum.flag = true;
//通知 消费者线程吃包子
bum.notify();
}
}
}
}
//消费者
class Customer extends Thread {
private Bum bum;
public Customer(String name, Bum bum) {
super(name);
this.bum = bum;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
//同步
synchronized (bum){
//根据flag判断包子是否存在,如果不存在则线程等待
if(bum.flag == false){
try {
bum.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("第" + i + "次," + this.getName() + " 开始吃包子...");
bum.num--;
System.out.println("消费完成,包子数量: " + bum.num + ",快生产吧!");
//消费完成,修改flag存在标识为false
bum.flag = false;
//通知 消费者线程吃包子
bum.notify();
}
}
}
}
//测试类
public class Test17_TwoCommunication {
public static void main(String[] args) {
//准备共享对象
Bum bum = new Bum();
//生产者线程
Thread th1 = new Producer("打工人",bum);
//消费者线程
Thread th2 = new Customer("吃货",bum);
//启动线程
th1.start();
th2.start();
}
}
案例展示:多线程案例:
//包子类
class Bum2 {
// 包子数量
int num = 0;
// 线程执行标识: 0表示线程1执行 1表示线程2执行 2表示线程3执行
int flag = 0;
}
//生产者
class Producer1 extends Thread {
private Bum2 bum;
public Producer1(String name, Bum2 bum) {
super(name);
this.bum = bum;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
// 同步
synchronized (bum) {
// 根据flag判断包子是否存在,如果存在则 线程进行等待
// 注意,此处必须改为while,用if无法实现功能
while (bum.flag != 0) {
try {
bum.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产包子
System.out.println("第" + i + "次," + this.getName() + ": 开始生产包子...");
// 每次生产2个包子
bum.num += 2;
System.out.println("生产完成,包子数量: " + bum.num + ",快来吃!");
// 生产完成,修改flag存在标识为true
bum.flag = 1;
// 通知 其他所有线程转入运行
bum.notifyAll();
}
}
}
}
//消费者线程
class Customer2 extends Thread {
private Bum2 bum;
public Customer2(String name, Bum2 bum) {
super(name);
this.bum = bum;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
// 同步
synchronized (bum) {
// 根据flag判断包子是否存在,如果不存在则线程等待
// 注意,此处必须改为while,用if无法实现功能
while (bum.flag != 1) {
try {
bum.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.getName() + " 开始吃包子...");
bum.num--;
System.out.println("消费完成,包子剩余数量: " + bum.num);
// 消费完成,修改flag存在标识为false
bum.flag = 2;
// 通知 其他所有线程转入运行
bum.notifyAll();
}
}
}
}
class Customer3 extends Thread {
private Bum2 bum;
public Customer3(String name, Bum2 bum) {
super(name);
this.bum = bum;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
// 同步
synchronized (bum) {
// 根据flag判断包子是否存在,如果不存在则线程等待
// 注意,此处必须改为while,用if无法实现功能
while (bum.flag != 2) {
try {
bum.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.getName() + " 开始吃包子...");
bum.num--;
System.out.println("消费完成,包子剩余数量: " + bum.num);
// 消费完成,修改flag存在标识为false
bum.flag = 0;
// 通知 其他所有线程转入运行
bum.notifyAll();
}
}
}
}
//测试类:
public class Test17_MoreCommunication {
public static void main(String[] args) {
Bum2 bum = new Bum2();
// 生产者线程
Thread th1 = new Producer1("打工人", bum);
// 消费者线程
Thread th2 = new Customer2("1号吃货", bum);
Thread th3 = new Customer3("2号吃货", bum);
th1.start();
th2.start();
th3.start();
}
}
13、死锁:
简单的描述死锁就是:俩个线程t1和t2,t1拿着t2需要等待的锁不释放,而t2又拿着t1需要等待的锁不释放,俩个线程就这样一直僵持下去。
是指两个或多个线程在执行的过程中,因争夺资源而陷入互相等待的状态,且没有任何一个线程可以主动释放资源,导致所有线程无法继续执行。
案例展示:
//结论:不要嵌套上锁(synchronized)
//容易造成死锁
public class Test18_DeadLock {
public static void main(String[] args) {
Object obj1 = new Object();
Object obj2 = new Object();
Thread th1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (obj1) {
System.out.println("th1拿到左筷子");
synchronized (obj2) {
System.out.println("th1拿到右筷子");
System.out.println("th1吃 水盆羊肉");
}
}
}
}
};
Thread th2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (obj2) {
System.out.println("th2拿到右筷子");
synchronized (obj1) {
System.out.println("th2拿到左筷子");
System.out.println("th2吃到了水盆羊肉");
}
}
}
}
};
th1.start();
th2.start();
}
}
建议不要随意嵌套上锁,但是当我们制定了合理的要求是,比如任何一个线程执行时都必须先拿到obj1才可以拿obj2,这样嵌套可以实现我们的功能,还防止了死锁的发生
案例展示:
public class Transfer {
public boolean transfer(Account from, Account to, double amount) {
// 验证转账金额是否有效
if (amount <= 0) {
System.out.println("转账金额必须大于0,转账失败");
return false;
}
// 按账户名称字典序排序,确定加锁顺序,避免死锁
Account firstLock = from;
Account secondLock = to;
if (from.getName().compareTo(to.getName()) > 0) {
firstLock = to;
secondLock = from;
}
// 按照排序后的顺序加锁
synchronized (firstLock) {
synchronized (secondLock) {
// 验证转出账户余额是否充足
if (from.getBalance() < amount) {
System.out.println(from.getName() + "余额不足,转账失败。余额:" + from.getBalance() + ",需转账:" + amount);
return false;
}
// 执行转账操作
from.setBalance(from.getBalance() - amount);
to.setBalance(to.getBalance() + amount);
// 输出转账结果
System.out.println(from.getName() + "向" + to.getName() +
"转账" +amount + "元," + from.getName()
+"余额:" + from.getBalance()+ "," + to.getName()
+ "余额:" + to.getBalance());
return true;
}
}
}
}
上述例子是转账时,永远都先给名字排序靠前的人先上锁,这样不管是A给B转,还是B给A,线程都要先拿到A、B中名字排序靠前的人的锁,就不会出现,有人拿了A的锁,有人拿了B的锁,造成死锁。
14、线程池:
(1)概念:
可以容纳多个线程的容器,其中的线程可以重复使用。
(2)优点:
1. 降低资源消耗: 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2. 提高响应速度 :当任务到达时,任务可以不需要的等到线程创建就能立即执行。
3. 提高线程的可管理性
(3)线程池的使用:
Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。
创建线程池的方法:
- public static ExecutorService newFixedThreadPool(int nThreads)
返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最 大数量)
使用线程池对象方法:
- public Future<?> submit(Runnable task)
想线程池提交一个任务,并返回一个future对象进行跟踪
Future接口:用来记录线程任务执行完毕后产生的结果
future对象.get():阻塞当前进程直到任务完成
future对象.cancel():任务未执行前取消
future对象.isDone():判断是否执行完毕
线程池操作步骤:
1. 创建线程池对象(ExecutorService类对象)
2. 创建Runnable接口子类对象
3. 提交Runnable接口子类对象(借助submit方法实现)
15、callable类:用于创建线程(有兴趣的自己了解)
更多推荐
所有评论(0)