多线程
1.相关概念[*]步伐(program):为完成特定任务,用某种语言编写的一组指令的聚集。即指一段静态的代码,静态对象
[*]进程(process):步伐的一次执行过程,或是正在内存中运行的应用步伐。如:运行中的QQ,运行中的网易音乐播放器。
[*]线程(thread):进程可进一步细化为线程,是步伐内部的一条执行路径。一个进程中至少有一个线程。
[*]进程同一时间若并行执行多个线程,就是支持多线程的。
2.创建和启动线程
2.1 方式1:继承Thread类
Java通过继承Thread类来创建并启动多线程的步骤如下:
[*]定义Thread类的子类,并重写该类的 run() 方法,该 run() 方法的方法体就代表了线程必要完成的任务
[*]创建 Thread 子类的实例,即创建了线程对象
[*]调用线程对象的 start() 方法来启动该线程
代码如下:
package Thread;
public class ThreadByThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
测试类:
package Thread;
public class ThreadTestByThread {
public static void main(String[] args) {
ThreadByThread t1 = new ThreadByThread();
ThreadByThread t2 = new ThreadByThread();
t1.start();
t2.start();
}
}
结果:发现卖出近100张票。
问题:但是有重复票或负数票问题。
原因:线程安全问题
解决方法:同步机制
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在
票问题,Java中提供了同步机制 (synchronized)来解决。
https://img2024.cnblogs.com/blog/3406667/202404/3406667-20240430154256840-887352134.png
根据案例简述:
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结
束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改
共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢
夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现
象。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。注意:在任
何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能
在外等着 (BLOCKED) 。
同步机制的原理,其实就相称于给某段代码加“锁”,任何线程想要执行这
段代码,都要先得到“锁”,我们称它为同步锁。因为Java对象在堆中的数
据分为分为对象头、实例变量、空白的填充。而对象头中包含:
[*]Mark Word:记载了和当前对象有关的GC、锁标记等信息。
[*]指向类的指针:每一个对象必要记载它是由哪个类创建出来的。
[*]数组长度(只有数组对象才有)
哪个线程得到了“同步锁”对象之后,”同步锁“对象就会记载这个线程的
ID,这样其他线程就只能等待了,除非这个线程”开释“了锁对象,其他
线程才能重新得到/占"同步锁"对象。
同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块
的资源实行互斥访问。 格式:
代码如下:
package Thread;
public class ThreadByRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
测试类:
package Thread;
public class ThreadTestByRunnable {
public static void main(String[] args) {
ThreadByRunnable r = new ThreadByRunnable();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能
进入这个方法,其他线程在外面等着。
new Thread("新的线程!"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}.start();
静态方法加锁代码:
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":" + i);
}
}
}).start();
非静态方法加锁:
package unsafe;
class TicketSaleThread extends Thread {
private static int ticket = 100;
public void run() {
while (ticket > 0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "卖出一张票,票号:" + ticket);
ticket--;
}
}
}
public class SaleTicket {
public static void main(String[] args) {
TicketSaleThread t1 = new TicketSaleThread();
TicketSaleThread t2 = new TicketSaleThread();
TicketSaleThread t3 = new TicketSaleThread();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
6. 再谈同步
6.1 死锁
差别的线程分别占用对方必要的同步资源不放弃,都在等待对方放弃自己必要
的同步资源,就形成了线程的死锁。
https://img2024.cnblogs.com/blog/3406667/202404/3406667-20240430160718546-454226953.png
诱发死锁的原因:
[*]互斥条件
[*]占用且等待
[*]不可抢夺(或不可抢占)
[*]循环等待
以上4个条件,同时出现就会触发死锁。
解决死锁:
死锁一旦出现,基本很难人为干预,只能只管规避。可以思量打破上面的诱发条件。
针对条件1:互斥条件基本上无法被破坏。因为线程必要通过互斥解决安全问题。
针对条件2:可以思量一次性申请全部所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就自动开释掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
6.2 JDK5.0 新特性:LOCK(锁)
[*]JDK5.0的新增功能,保证线程的安全。与采用synchronized相比,Lock可
提供多种锁方案,更灵活、更强大。Lock通过显式定义同步锁对象来实现同
步。同步锁使用Lock对象充当。
[*]java. util. concurrent. locks. Lock 接口是控制多个线程对共享资源进行访
问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对
象加锁,线程开始访问共享资源之前应先得到Lock对象。
[*]实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、开释锁。
[*]ReentrantLock类实现了 Lock 接口,它拥有与 synchronized 类似的
并发性和内存语义,但是添加了类似锁投票、定时锁等候和可停止锁等
候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
[*]Lock锁也称同步锁,加锁与开释锁方法,如下:
[*]public void lock() :加同步锁。
[*]public void unlock() :开释同步锁。
代码如下:
package safe;
class TicketSaleRunnable implements Runnable {
private int ticket = 100;
public void run() {
while (ticket > 0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
ticket--;
}
}
}
public class SaleTicket {
public static void main(String[] args) {
TicketSaleRunnable tr = new TicketSaleRunnable();
Thread t1 = new Thread(tr, "窗口一");
Thread t2 = new Thread(tr, "窗口二");
Thread t3 = new Thread(tr, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
synchronized 与 Lock 的对比
[*]Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域、遇到异常等自动解锁
[*]Lock 只有代码块锁,synchronized 有代码块锁和方法锁
[*]使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更表现面向对象。
[*](了解)Lock 锁可以对读不加锁,对写加锁,synchronized 不可以
[*](了解)Lock 锁可以有多种获取锁的方式,可以从 sleep 的线程中抢到锁,synchronized 不可以
说明:开辟建议中处理线程安全问题优先使用顺序为:
Lock ----> 同步代码块 ----> 同步方法
7. 线程的通讯
生产者与消耗者问题
等待唤醒机制可以解决经典的“生产者与消耗者”的问题。生产者与消耗者问
题,也称有限缓冲问题,是一个多线程同步问题的经典案例。该问题描述了两个
(多个)共享固定巨细缓冲区的线程——即所谓的“生产者”和“消耗者”——
在实际运行时会发生的问题。
生产者的主要作用是天生肯定量的数据放到缓冲区中,然后重复此过程。与此
同时,消耗者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在
缓冲区满时参加数据,消耗者也不会在缓冲区中空时消耗数据。
举例:
生产者(Productor)将产品交给伙计(Clerk),而消耗者(Customer)从伙计
处取走产品,伙计一次只能持有固定数量的产品(比如:20),如果生产者试
图生产更多的产品,伙计会叫生产者停一下,如果店中有空位放产品了再通
知生产者继续生产;如果店中没有产品了,伙计会告诉消耗者等一下,如果
店中有产品了再通知消耗者来取走产品。
类似的场景,比如厨师和服务员等。
生产者与消耗者问题中其实隐含了两个问题:
[*]线程安全问题:
因为生产者与消耗者共享数据缓冲区,产生安全问题。不过这个问题可以使
用同步解决。
[*]线程的和谐工作问题:
要解决该问题,就必须让生产者线程在缓冲区满时等待(wait),暂停进入阻
塞状态,等到下次消耗者消耗了缓冲区中的数据的时候,通知(notify)正在
等待的线程恢复到就绪状态,重新开始往缓冲区添加数据。同样,也可以让
消耗者线程在缓冲区空时进入等待(wait),暂停进入阻塞状态,等到生产者
往缓冲区添加数据之后,再通知(notify)正在等待的线程恢复到就绪状态。
通过这样的通讯机制来解决此类问题。
代码实现:
synchronized(同步锁){
需要同步操作的代码
}
8. JDK5.0 新增线程创建方式
8.1 新增方式一:实现 Callable接口
[*]与使用Runnable相比, Callable功能更强大些
– 相比run()方法,可以有返回值
– 方法可以抛出异常
– 支持泛型的返回值(必要借助FutureTask类,获取返回结果)
[*]Future接口(了解)
– 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
– FutureTask是Futrue接口的唯一的实现类
– FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
[*]缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。
代码举例:
/* * 创建多线程的方式三:实现Callable (jdk5.0新增的) *///1.创建一个实现Callable的实现类class NumThread implements Callable { //2.实现call方法,将此线程必要执行的操作声明在call()中 @Override public Object call() throws Exception { int sum = 0; for (int i = 1; i
页:
[1]