Java多线程学习(全面万字长篇)
- 前言
- 线程的创建和使用
-
- 线程的创建和启动
- 创建线程的两种方式
- Thread类相关方法
- JDK5.0新增创建线程方式
- 线程的优先级
- 线程的生命周期
- 线程的同步(解决共享资源竞争)
-
- 方式一:同步代码块
- 方式二:同步方法
- 方式三:使用显示的Lock对象
- synchronized 与 Lock 的对比
- 线程的通信
- 生产者和消费者问题
前言
本篇文章只是对多线程做一个简单较全面了解,并不深入探讨
什么时候需要使用多线程?
1、当程序需要同时执行两个或多个任务时。
2、程序需要实现一些需要等待的任务时,如用户输入、文件读写
操作、网络操作、搜索等。
3、需要一些后台运行的程序时。
线程的创建和使用 线程的创建和启动
- Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread
类来体现。 - Thread类的特性
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常
把run()方法的主体称为线程体 - 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()创建线程的两种方式
- 定义子类继承Thread类。
- 子类中重写Thread类中的run方法。
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法:启动线程,调用run方法。
class Even extends Thread{
@Override
public void run(){
for(int i=0;
i<10;
i++){
if(i%2 != 0)
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
方式二:实现Runnable接口
- 定义子类,实现Runnable接口。
- 子类中重写Runnable接口中的run方法。
- 通过Thread类含参构造器创建线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
class Odd implements Runnable{
@Override
public void run() {
for(int i=0;
i<10;
i++){
if(i%2==0)
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.yield();
//让步
/*try {
Thread.sleep(100);
//休眠
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
}
}
public class ThreadDome01 {
public static void main(String[] args) {
Even even = new Even();
even.start();
Odd odd = new Odd();
Thread thread = new Thread(odd);
thread.start();
}
}
/* [补充]:1.在run()执行完之后,线程就死亡了
*2.even.start()已经创建了一个线程,不能再写even.start(),如果要再创建
*线程,就必须再new一个对象
*/
输出结果:
Thread-1:0
Thread-0:1
Thread-0:3
Thread-1:2
Thread-0:5
Thread-1:4
Thread-0:7
Thread-1:6
Thread-0:9
Thread-1:8
推荐使用实现Runnable的方式:
- 避免了单继承的局限性
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线
程来处理同一份资源。
- void start() : 启动线程,并执行对象的run()方法
- run(): 线程在被调度时执行的操作
- String getName(): 返回线程的名称
- void setName(String name):设置该线程名称
- static Thread currentThread(): 返回当前线程。在Thread子类中就
是this,通常用于主线程和Runnable实现 - static void yield():线程让步
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法 - join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将
被阻塞,直到 join() 方法加入的 join 线程执行完为止
- 低优先级的线程也可以获得执行 - static void sleep(long millis):(指定时间:毫秒)
令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后
重排队。
线程池相关API:
- JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
- < T> Future< T > submit(Callable< T> task):执行任务,有返回值,一般又来执行Callable
- void shutdown() :关闭连接池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n);
创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
java SE5的Java.util.concurrent包中的执行器可以为我们管理Thread对象,从而简化并发编程.Executor无须显示地管理线程的生命周期。
class Ex3RunnerA implements Runnable {
public Ex3RunnerA() {
System.out.println("Constructing Ex3RunnerA");
}
public void run() {
for(int i = 0;
i < 3;
i++) {
System.out.println("Hi from Ex3RunnerA");
Thread.yield();
}
System.out.println("Ex3RunnerA task complete.");
// return;
}
}class Ex3RunnerB implements Runnable {
public Ex3RunnerB() {
System.out.println("Constructing Ex3RunnerB");
}
public void run() {
for(int i = 0;
i < 3;
i++) {
System.out.println("Hi from Ex3RunnerB");
Thread.yield();
}
System.out.println("Ex3RunnerB task complete.");
//return;
}
}class Ex3RunnerC implements Runnable {
public Ex3RunnerC() {
System.out.println("Constructing Ex3RunnerC");
}
public void run() {
for(int i = 0;
i < 3;
i++) {
System.out.println("Hi from Ex3RunnerC");
Thread.yield();
}
System.out.println("Ex3RunnerC task complete.");
//return;
}
}
public class Ex3 {
public static void main(String[] args) {
// ExecutorService对象是通过Executors的静态方法创造的
ExecutorService exec1 = Executors.newCachedThreadPool();
exec1.execute(new Ex3RunnerA());
exec1.execute(new Ex3RunnerB());
exec1.execute(new Ex3RunnerC());
exec1.shutdown();
ExecutorService exec2 = Executors.newFixedThreadPool(3);
exec2.execute(new Ex3RunnerA());
exec2.execute(new Ex3RunnerB());
exec2.execute(new Ex3RunnerC());
exec2.shutdown();
ExecutorService exec3 = Executors.newSingleThreadExecutor();
//就像是线程数为1的FixedThreadPool
exec3.execute(new Ex3RunnerA());
exec3.execute(new Ex3RunnerB());
exec3.execute(new Ex3RunnerC());
exec3.shutdown();
}
}
新增方式二:实现Callable接口
与使用Runnable相比, Callable功能更强大些
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结
class TaskWithResult implements Callable {
private int id;
public TaskWithResult(int id){
this.id = id;
}
@Override
public String call(){
return "result of TaskWithResult "+id;
}
}
public class Ex5 {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
ArrayList> results= new ArrayList<>();
for(int i=0;
i<10;
i++){
results.add(exec.submit(new TaskWithResult(i)));
//submit()方法会产生Future对象
}
for(Future fs : results){
try {
System.out.println(fs.get());
//返回的结果
}catch (InterruptedException e){
System.out.println(e);
return;
}catch (ExecutionException e){
System.out.println(e);
return;
}finally {
exec.shutdown();
}
}
}
}
submit()方法会产生Future对象,它用Callable返回的结果的特定类型进行了参数化,可以用isDone()方法来查询Future是否已经完成。当完成任务时,它具有一个结果,可以调用get()来获取该结果。
输出结果:
result of TaskWithResult 0
result of TaskWithResult 1
result of TaskWithResult 2
result of TaskWithResult 3
result of TaskWithResult 4
result of TaskWithResult 5
result of TaskWithResult 6
result of TaskWithResult 7
result of TaskWithResult 8
result of TaskWithResult 9
线程的优先级
- 线程的优先级等级
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5 - 涉及的方法
- getPriority() :返回线程优先值
- setPriority(int newPriority) :改变线程的优先级
- 说明
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
文章图片
线程的同步(解决共享资源竞争)
首先举个例子——模拟火车站售票程序,开启三个窗口售票。
class Ticket implements Runnable {
private int tick = 100;
public void run() {
while (true) {
if (tick > 0) {
try {
Thread.sleep(100);
//将线程安全问题展现更清楚
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread
().getName() + "售出车票,tick号为:" + tick--);
} else
break;
}
}
}
class TicketDemo {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("t1窗口");
t2.setName("t2窗口");
t3.setName("t3窗口");
t1.start();
t2.start();
t3.start();
}
}
部分输出结果:
t2窗口售出车票,tick号为:4
t1窗口售出车票,tick号为:4
t3窗口售出车票,tick号为:3
t1窗口售出车票,tick号为:3
t2窗口售出车票,tick号为:1
t3窗口售出车票,tick号为:2问题:多个窗口售票出了相同的票号。
问题原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
防止线程冲突的方法就是当资源被一个任务使用时,在其上加上锁。都一般采取序列化访问共享资源 的方案。
方式一:同步代码块
方式一:同步代码块
*synchronized(同步监视器){
*//需要同步的代码
*}
*说明:1、需要同步的代码即使操作共享数据的代码
*2、共享数据:多个线程共同操作的变量,在本例子中tick
*3、同步监视器,俗称:锁。任何一个类的对象,都可以充当索。
*要求:多个线程必须要共用同一把锁。
使用同步代码块的方式来改改上面的火车卖票的程序。
下面是实现Runnable接口的方式
class Ticket implements Runnable {
private int tick = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
// synchronized (this){//对于实现同步监视器是当前对象t。一般都写this
synchronized (obj) {//同步监视器是obj,任何对象都可以当作索。但必须多个线程共用一把锁
if (tick > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread
().getName() + "售出车票,tick号为:" + tick--);
} else
break;
}
//}
}
}
}
需要注意的是同步监视器(也就是锁),同步监视器可以是任何对象,但必须是多个线程共用一把锁。对于实现Runnable接口的方式,上面两个都可以,一般用synchronized (this){},
下面是对于继承的方式
继承的方式和实现接口的方式是有所不同的,
由于继承方式需要创建3个Ticcket1对象,所以不能用synchronized (this){},
可以将Ticket1类作为同步监视器,即 synchronized (Ticket1.class){ },也可以用其它对象,但是需要声明为静态static的。
class Ticket1 extends Thread {
private static int tick = 100;
// private static Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (Ticket1.class){//和下面的都可以
//synchronized (obj) {//同步监视器是obj,
if (tick > 0) {
try {
Thread.sleep(100);
//将线程安全问题展现更清楚
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread
().getName() + "售出车票,tick号为:" + tick--);
} else
break;
}
//}
}
}
}class TicketDemo02 {
public static void main(String[] args) {
Ticket1 t1 = new Ticket1();
Ticket1 t2 = new Ticket1();
Ticket1 t3 = new Ticket1();
t1.setName("t1窗口");
t2.setName("t2窗口");
t3.setName("t3窗口");
t1.start();
t2.start();
t3.start();
}
}
总的来说就是同步监视器需要是多个线程共用一个
方式二:同步方法 同步方法就是如果操作共享资源的刚好是一个方法,即可以在方法上加上synchronized 关键字就行。
比如对于实现的方式
public synchronized void method(){}
对于继承的方式 需要加上static。
public static synchronized void show(){}
对于同步方法的总结:
- 同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
- 非静态的同步方法,同步监视器是:this
- 静态的同步方法,同步监视器是:当前类本身。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的
工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象
加锁,线程开始访问共享资源之前应先获得Lock对象。
class A{
private ReentrantLock lock = new ReenTrantLock();
//private Lock lock = new ReenTrantLock();
//也可以
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}
finally{
lock.unlock();
}
}
}
//注意:如果同步代码有异常,要将unlock()写入finally语句块
注意 如果方法有返回值,return必须在try子句中出现,以确保unlock()不会过早发生。
synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是
隐式锁,出了作用域自动释放 - Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有
更好的扩展性(提供更多的子类)
优先使用顺序:
Lock ——> 同步代码块(已经进入了方法体,分配了相应资源) ——> 同步方法
(在方法体之外)
还是以一个例子来讲解:使用两个线程打印 1-100。线程1, 线程2 交替打印
class Communication implements Runnable {
int i = 1;
public void run() {
while (true) {
synchronized (this) {
notify();
//因为省略了this,相当于this.notify(),必须是同步监视器调用。
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i++);
} else
break;
try {
wait();
//省略了this,相当于this.wait(),必须是同步监视器调用。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Communication c = new Communication();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
涉及到的3个方法:
- wait():一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器。
- notify(): 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait(),就唤醒优先级较高的。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
这三个方法必须使用在同步代码块或同步方法中。
这三个方法的调用者必须是同步代码块或同步方法中的同步监视器
否则都会报java.lang.IllegalMonitorStateException异常。
如果同步监视器是其它对象:
private Object obj = new Object();
//省略代码...
synchronized (obj) {//obj作为同步监视器
obj.notify();
//省略代码...
obj.wait();
//....
}
正因为如此,这三个方法声明在Object类中。
再来说一说wait()和sleep()的区别:
- 两个方法声明位置不同。Thread类中声明sleep(),Object类中声明wait();
- sleep()可以在任何需要的场景使用,而wait()必须使用在同步代码块或同步方法中
- 如果两个方法都是用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放同步监视器。
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,
店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;
如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
代码:
class Clerk { // 售货员
private int product = 0;
public synchronized void addProduct() {
if (product >= 20) {
try {
wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}
} else {
product++;
System.out.println("生产者生产了 第" + product + "个产品");
notifyAll();
}
}
public synchronized void getProduct() {
if (this.product <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("消费者取走了第" +
product + "个产品");
product--;
notifyAll();
}
}
}
class Productor implements Runnable { // 生产者
Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("生产者开始生产产品");
while (true) {
try {
Thread.sleep((int) Math.random() * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.addProduct();
}
}
}
class Consumer implements Runnable { // 消费者
Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("消费者开始取走产品");
while (true) {
try {
Thread.sleep((int) Math.random() * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.getProduct();
}
}
}public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Thread productorThread = new Thread(new Productor(clerk));
Thread consumerThread = new Thread(new Consumer(clerk));
productorThread.start();
consumerThread.start();
}}
资料:宋红康java多线程讲解
【Java|Java多线程学习总结(全面的万字长篇)】如果读者大大们感觉写的可以,希望给个小小的点赞,你们的鼓励就是我前进的动力。
推荐阅读
- Java 8 判空新写法。。
- 舒服了,踩到一个关于分布式锁的非比寻常的BUG!
- 学习笔记|SpringBoot05(自动配置原理)
- java|Spring IOC 深入浅出
- Java|长度最小的子数组(Java)
- Android多线程断点续传下载原理及实现
- Android多线程——View.post()源码分析
- Java数据结构|《Java数据结构基础》单链表的手动实现
- 面试题|【Java实习生面试题系列】-- 多线程篇四