Java多线程笔记
基本概念
程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process):是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。–生命周期
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间 并行执行多个线程,就是支持多线程的。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
- 一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
线程的创建和使用
方式一:继承Thread类
1)定义子类继承Thread类。
2)子类中重写Thread类中的run方法。
3)创建Thread子类对象,即创建了线程对象。
4)调用线程对象start方法:启动线程,调用run方法。
注意点:
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
- 想要启动多线程,必须调用start方法。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出非法线程状态的异常”IllegalThreadStateException”。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class ThreadTest { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); MyThread thread2 = new MyThread(); thread2.start(); for (int i = 0; i < 1000; i++) { System.out.println(i+"---- main ----"); } } }
class MyThread extends Thread{
@Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } } }
|
方式二:实现Runnable接口
1) 创建一个实现Runnable接口的类。
2) 在实现类中实现Runnable接口中的run()抽象方法。
3) 创建实现类的对象。
4) 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象。
5) 通过Thread类的对象调用start()
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class ThreadTest2 { public static void main(String[] args) { MyThread thread = new MyThread();
new Thread(thread).start(); } }
class MyThread implements Runnable {
@Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } }
|
继承Thread方式和实现Runnable方式的联系与区别
区别
实现Runnable方式的好处(优先选择)
- 避免了单继承的局限性。
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
方式三:实现Callable接口(JDK5.0新增)
与使用Runnable相比,Callable功能更强大些
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
使用步骤:
创建Callable接口的实现类
实现call方法,将此线程需要执行的操作声明在call()中
创建Callable接口实现类的对象
将此Callable接口实现类的对象传递给FutureTask的构造器中,创建FutureTask的对象
将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()启动线程
获取Callable中call()的返回值,该操作需要在启动线程后进行,否则将卡在此处。
例:利用线程获取0~100中偶数的返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask;
class NumberCallable implements Callable {
@Override public Object call() throws Exception { int sum = 0; for (int i = 0; i <= 100; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + ":" + i); sum += i; } } return sum; } }
public class CallableThreadTest { public static void main(String[] args) { NumberCallable number = new NumberCallable(); FutureTask task = new FutureTask(number); new Thread(task).start(); try { Object sum = task.get(); System.out.println("sum:" + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
|
方式四:使用线程池(JDK5.0新增)
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池相关API
JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
- void shutdown() :关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
使用ThreadPoolExecutor设置线程池参数
1 2 3 4 5 6 7
| import java.util.concurrent.ThreadPoolExecutor;
ExecutorService executorService = Executors.newFixedThreadPool(10); ThreadPoolExecutor executor = (ThreadPoolExecutor)executorService;
executor.setCorePoolSize(15); executor.setKeepAliveTime(3600, TimeUnit.SECONDS);
|
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future;
class NumberRunnable implements Runnable {
@Override public void run() { for (int i = 0; i < 1000; i++) { if (i % 2 != 0) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } }
public class ThreadPoolTest { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new NumberRunnable()); Future future = executorService.submit(new NumberCallable()); try { System.out.println(Thread.currentThread().getName() + ":Total:" + future.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } executorService.shutdown(); } }
|
Thread中常用方法
1.void start()
启动线程,并执行对象的run()方法
2.run()
线程在被调度时执行的操作,通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3.static currentThread()
返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
4.getName()/setName()
返回/设置线程的名称
5.static void yield()
线程让步,释放当前CPU的执行权
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
6.join()
当某个程序执行流中调用其他线程的join()
方法时,调用线程将被阻塞,直到join()
方法加入的join
线程执行完为止。
也就是说,在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完毕以后,线程a才结束阻塞状态。
7.stop() “Deprecated”
强制线程生命期结束,不推荐使用
8.static void sleep(long millis) :(指定时间:毫秒)
令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
就是让当前线程“睡眠”指定的millis毫秒,在指定的millis毫秒时间内,当前线程是阻塞状态。
9.boolean isAlive()
返回boolean,判断线程是否还活着
线程的优先级
线程的优先级等级
- MAX_PRIORITY :10
- MIN _PRIORITY :1
- NORM_PRIORITY :5
涉及的方法
- getPriority():返回线程优先值
- setPriority(int newPriority):改变线程的优先级
说明
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
线程的分类
Java中的线程分为两类:一种是守护线程,一种是用户线程。
- 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
- 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
- Java垃圾回收就是一个典型的守护线程。
- 若JVM中都是守护线程,当前JVM将退出。
- 形象理解:兔死狗烹,鸟尽弓藏
线程的生命周期
JDK 中用Thread.State
类定义了线程的几种。
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread
类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的 五种状态:
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
![image-20210331231930572](image-20210331231930572.png)
当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。
让子线程立即执行的方法:如果希望调用子线程的start()方法后子线程立即开始执行,程序可以使用Thread.sleep(1)
来让当前运行的线程(主线程)睡眠1毫秒——1毫秒就够了,因为在这1毫秒内CPU不会空闲,它会去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行。
线程的同步
方式一:同步代码块
说明:
操作共享数据的代码,即为需要被同步的代码。不能包含代码多了,也不能包含代码少了。
共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
在实现Runnable接口创建多线程的方式中,可以考虑使用this充当同步监视器。
在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类(类名.class)充当同步监视器。
例:
- 在实现Runnable接口中使用同步代码块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class SellTicketTest { public static void main(String[] args) { SellTickets tickets = new SellTickets(); Thread window1 = new Thread(tickets); Thread window2 = new Thread(tickets); Thread window3 = new Thread(tickets);
window1.setName("Window 1"); window2.setName("Window 2"); window3.setName("Window 3");
window1.start(); window2.start(); window3.start(); } }
class SellTickets implements Runnable { private int tickets = 1000; private Object lock = new Object();
@Override public void run() {
while (true) { synchronized (lock) { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets); tickets--; } else { break; } } }
} }
|
- 在继承Thread类中使用同步代码块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public class SellTicketTest2 { public static void main(String[] args) { Thread window1 = new SellTickets2(); Thread window2 = new SellTickets2(); Thread window3 = new SellTickets2();
window1.setName("Window 1"); window2.setName("Window 2"); window3.setName("Window 3");
window1.start(); window2.start(); window3.start(); } }
class SellTickets2 extends Thread { private int tickets = 1000; private static Object lock = new Object();
@Override public void run() { while (true) { synchronized (lock) {
if (tickets > 0) { System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets); tickets--; } else { break; } } } } }
|
方式二:同步方法
如果操作共享数据的代码证号完整的声明在一个方法中,可以将此方法声明为同步方法。
同步监视器:
非静态的同步方法:this
静态的同步方法:类名.class
例:
非静态的同步方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public class SellTicketTest3 { public static void main(String[] args) { SellTickets3 tickets = new SellTickets3(); Thread window1 = new Thread(tickets); Thread window2 = new Thread(tickets); Thread window3 = new Thread(tickets);
window1.setName("Window 1"); window2.setName("Window 2"); window3.setName("Window 3");
window1.start(); window2.start(); window3.start(); } }
class SellTickets3 implements Runnable { private int tickets = 100;
@Override public void run() {
while (hasTicket()) { } } private synchronized boolean hasTicket() { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets); tickets--; return true; } else { return false; } } }
|
静态的同步方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public class SellTicketTest4 { public static void main(String[] args) {
Thread window1 = new SellTickets4(); Thread window2 = new SellTickets4(); Thread window3 = new SellTickets4();
window1.setName("Window 1"); window2.setName("Window 2"); window3.setName("Window 3");
window1.start(); window2.start(); window3.start(); } }
class SellTickets4 extends Thread { private static int tickets = 100;
@Override public void run() { while (hasTicket()) { } }
private static synchronized boolean hasTicket() { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets); tickets--; return true; } else { return false; } } }
|
方式三:Lock锁🔒(JDK 5.0之后)
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制,比较常用的是ReentrantLock,可以显式加锁、释放锁。
使用方法:
- 创建锁:private static ReentrantLock lock = new ReentrantLock();
- 调用锁定方法:lock()
- 调用解锁方法:unlock()
使用时,使用try...finally...
进行包裹,在try中进行上锁,在finally中进行解锁,以保证锁一定会被释放。
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| import java.util.concurrent.locks.ReentrantLock;
public class SellTicketTest5 { public static void main(String[] args) {
Thread window1 = new SellTickets5(); Thread window2 = new SellTickets5(); Thread window3 = new SellTickets5();
window1.setName("Window 1"); window2.setName("Window 2"); window3.setName("Window 3");
window1.start(); window2.start(); window3.start(); } }
class SellTickets5 extends Thread { private static int tickets = 100; private static ReentrantLock lock = new ReentrantLock();
@Override public void run() { while (true) { try { lock.lock(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } if (tickets > 0) { System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets); tickets--; } else { break; } } finally { lock.unlock(); } } } }
|
synchronized与Lock的异同
相同点:二者都可以解决线程安全问题
不同点:synchronized机制在执行完相应的同步代码后,自动释放同步监视器(锁);Lock需要手动启动同步(lock()),结束同步也需要手动实现(unlock())
优先使用顺序
Lock ➡ 同步代码块(已经进入了方法体,分配了相应资源)➡ 同步方法(在方法体之外)
单例设计式模式之懒汉式(线程安全)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Singleton { private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
public class SingletonTest { public static void main(String[] args) { Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1 == s2); } }
|
死锁的问题
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
说明:
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
- 使用同步时,要避免出现死锁
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
线程的通信
相关的三个方法
wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll():唤醒正在排队等待资源的所有线程结束等待
说明:
- wait()、notify()、notifyAll()三个方法必须使用在synchronized代码块或synchronized方法中,lock锁的方式不适用。
- wait()、notify()、notifyAll()三个方法的调用者必须是synchronized代码块或synchronized方法中的同步监视器,否则会出现IllegalMonitorStateException异常。
- wait()、notify()、notifyAll()三个方法定义在java.lang.Object类中。因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。
基本使用
例:
使用两个线程交替打印打印1-100。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| public class ThreadCommunicationTest { public static void main(String[] args) { Number number = new Number();
Thread t1 = new Thread(number); Thread t2 = new Thread(number);
t1.setName("线程1"); t2.setName("线程2");
t1.start(); t2.start(); } }
class Number implements Runnable { private int number = 1; private Object lock = new Object(); @Override public void run() { while (true) { synchronized (lock) { lock.notify(); if (number <= 100) { System.out.println(Thread.currentThread().getName() + ":" + number); number++; try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { break; } } } } }
|
sleep()和wait()的异同
相同点:一旦执行方法,都可以使用当前的线程进入阻塞状态。
不同点:
1)两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
2) 调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须在同步代码块或同步方法中由同步监视器调用
3) 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
经典例题:生产者/消费者问题
题目:生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到。
- 消费者比生产者快时,消费者会取相同的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
|
public class ProducerConsumerTest { public static void main(String[] args) { Clerk clerk = new Clerk(); Producer producer = new Producer(clerk); Consumer consumer1 = new Consumer(clerk); Consumer consumer2 = new Consumer(clerk); producer.setName("生产者1"); consumer1.setName("消费者1"); consumer2.setName("消费者2"); producer.start(); consumer1.start(); consumer2.start(); } }
class Clerk { private int productCount = 0;
public synchronized void produceProduct() {
if (productCount < 20) { productCount++; System.out.println(Thread.currentThread().getName() + ":生产第" + productCount + "个产品"); notify(); } else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
}
public synchronized void consumeProduct() { if (productCount > 0) { System.out.println(Thread.currentThread().getName() + ":消费第" + productCount + "个产品"); productCount--; notify(); } else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }
}
class Producer extends Thread { private Clerk clerk;
public Producer(Clerk clerk) { this.clerk = clerk; }
@Override public void run() { System.out.println("开始生产..."); while (true) { try { Thread.sleep((int)(Math.random()*300)); } catch (InterruptedException e) { e.printStackTrace(); } clerk.produceProduct(); } } }
class Consumer extends Thread { private Clerk clerk;
public Consumer(Clerk clerk) { this.clerk = clerk; }
@Override public void run() { System.out.println("消费产品..."); while (true) { try { Thread.sleep((int)(Math.random()*1000)); } catch (InterruptedException e) { e.printStackTrace(); } clerk.consumeProduct(); } } }
|