JavaSE多线程笔记
文章目录
1、创建多线程的两种方式
方式一:继承Thread类
1、Thread类的特性
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作,经常把run()方法的主体称为线程体。
- 通过该Thread对象的start()方法来启动这个线程,并不是直接调用run()。
2、操作步骤
- 创建一个继承Thread类的子类
- 子类里重写Thread类的run(),方法体声明这个线程的执行操作。
- 创建子类的对象
- 通过子类对象调用start()
- start()的两个作用:① 启动当前线程;② 调用当前线程的run();
举例:在main方法的主线程里开启另一个线程去遍历100内的偶数
//继承Thread类
public class MyThread extends Thread{
//重写run(),把这个线程要执行的操作写在方法体里
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println("i = " + i);
}
}
}
}
//测试启动线程
public class test {
public static void main(String[] args) {
MyThread thread1 = new MyThread(); //创建MyThread对象
thread1.start(); //调用start()方法,启动另一个线程执行操作
//start()的两个作用:① 启动当前线程;② 调用当前线程的run();
//如果直接调用run()的话,就不会开启新的线程,而是正常在主线程里调用对象方法。
}
}
3、两个注意问题
创建线程的两个注意问题:
1、必须调用start()方法启动新线程。如果直接调用run()则不会开启新线程,这样只是在主线程里调用对象方法。
2、同一个Thread子类对象的start()方法只能调用一次,再调用就会报异常。要想创建多个线程,就创建多个Thread的子类对象去调用各自的start()。
4、Thread的相关方法
/** 启动线程,并执行run() */
void start();
/** 线程在被调度时执行的操作 */
run();
/** 返回线程名称 */
String getName();
/** 设置线程名称 */
void setName(String name); */
/** 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类 */
static Thread currentThread();
/** 线程让步,暂停线程,给优先级更高的线程让步(没有的话方法被忽略) */
static void yield();
/** 调用其他线程的join方法,然后自己阻塞,直到join进来的线程执行结束再取消自己的阻塞状态 */
join();
/** 指定睡眠时间,使其他线程有机会执行 */
static void sleep(long millis);
/** 强制结束线程,不推荐使用 */
stop();
/** 判断线程是否存活 */
boolean isAlive();
5、设置线程优先级
5.1 线程的调度
调度策略
- 时间片策略:轮流分配时间片;
- 抢占式策略:高优先级的线程抢占CPU;
Java的调度方法
- 同优先级组成先进先出队列,使用时间片策略;
- 对高优先级,使用优先调度的抢占式策略;
5.2 线程的优先级
线程优先级等级
public class Thread implements Runnable {
...
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5; //默认
public final static int MAX_PRIORITY = 10;
...
}
相关方法
getPriority(); //返回线程优先值
setPriority(int newPriority); //改变线程的优先级
说明
- 线程创建时继承父线程的优先级;
- 低优先级获得调度的概率低,但不一定必须在高优先级的线程之后才调用;
方式二:实现Runnable接口
1、操作步骤
- 创建类实现Runnable接口;
- 实现类实现Runnable中的抽象方法run();
- 创建实现类对象,将此对象传进Thread类的构造器;
- 创建Thread类的对象;
- Thread类对象调用start();
//创建类实现Runnable接口
public class MyThread implements Runnable {
//实现抽象方法run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println("i = " + i);
}
}
}
}
//测试启动线程
public class test {
public static void main(String[] args) {
//创建Runnable实现类对象
MyThread thread1 = new MyThread();
//传参给Thread构造器创建Thread对象
Thread t1 = new Thread(thread1);
//Thread对象调用start()启动线程
t1.start();
}
}
两种创建方式的比较
开发中优先选择实现Runnable的方式。原因:
- 实现的方式没有类的单继承性的局限;
- 实现的方式更适合处理多个线程有共享数据的情况;
联系:
- Thread类也继承了Runnable接口。
- 两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
2、线程的生命周期
- 新建:Thread类或其子类对象被声明并创建时,新生的线程对象处于新建状态。
- 就绪:新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时已具备运行条件,只缺少CPU资源。
- 运行:当就绪的线程被调度并获得CPU资源时,进入运行状态,run()方法定义了线程的操作和功能。
- 阻塞:在某种特殊情况下,被认为挂起或者执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态。
- 死亡:线程完成了它的全部工作或线程被提前强制终止或出现异常导致结束。
3、synchronized同步机制解决线程安全问题
同步可以解决线程安全问题,但会降低效率。
3.1 方式一:同步代码块
重点理解同步监视器和需要被同步的代码。
synchronized(同步监视器){
//需要被同步的代码
}
说明:
-
synchronized包住需要被同步的代码,即操作共享数据的代码。(不能多包也不能少包)
-
共享数据就是多个线程共同操作的变量。
-
同步监视器:俗称锁,或同步锁。
-
任何一个类的对象,都可以充当锁。
-
多个线程必须共用同一把锁。
-
若实现Runnable接口创建多线程,可以考虑用this充当同步锁。
-
若继承Thread类创建多线程,慎用this当锁,可以考虑用 *.class 充当同步锁。
-
底线就是必须共用同一把锁。
-
1、处理实现Runnable的线程安全
创建类实现Runnable
package com.chw.threaddemo.entity;
public class Window1 implements Runnable {
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
//使用同步代码块包裹共享数据
//synchronized(this)也可以
synchronized (obj) {
if (ticket > 0) {
try {
Thread.currentThread().sleep(100);//用于加大不安全情况出现的概率
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为 " + ticket);
ticket--;
} else {
break;
}
}
}
}
}
测试
package com.chw.threaddemo.entity;
public class WindowTest1 {
public static void main(String[] args) {
Window1 window1 = new Window1();//只创建一个对象,保证锁唯一和ticket属性唯一
Thread t1 = new Thread(window1);
Thread t2 = new Thread(window1);
Thread t3 = new Thread(window1);
t1.start();
t2.start();
t3.start();
}
}
启动WindowTest1的main方法可以观察到控制台的输出是符合要求的。
2、处理继承Thread的线程安全
创建类继承Thread
package com.chw.threaddemo.entity;
public class Window2 extends Thread {
private static int ticket = 100;
private static Object obj = new Object();//要求锁唯一,所以要加静态static
@Override
public void run() {
while (true) {
//synchronized(Window2.class)也可以
synchronized (obj) {
if (ticket > 0) {
try {
sleep(100);//用于加大不安全情况出现的概率
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为 : " + ticket);
ticket--;
} else {
break;
}
}
}
}
}
测试
package com.chw.threaddemo.entity;
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
启动WindowTest2的main方法可以观察到控制台的输出是符合要求的。
3.2 方式二:同步方法
如果操作共享数据的代码完整地声明在一个方法中,则可以将此方法声明为同步的。
在声明方法的修饰关键字里加上 synchronized 即可
synchronized void method(){...}
步骤简单概括:在Runnable实现类或者Thread子类下声明一个同步方法,同步方法里编写代码操作共享数据。再重写run()方法调用该同步方法即可。
1、处理实现Runnable的线程安全
创建类实现Runnable
package com.chw.threaddemo.entity;
public class Window3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();//调用同步方法
if (ticket < 1) {
break;
}
}
}
//声明同步方法
private synchronized void show() { //默认同步锁就是this
if (ticket > 0) {
try {
Thread.currentThread().sleep(50);//用于加大不安全情况出现的概率
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为 " + ticket);
ticket--;
}
}
}
测试
package com.chw.threaddemo.entity;
public class WindowTest3 {
public static void main(String[] args) {
Window3 window3 = new Window3();
Thread t1 = new Thread(window3);
Thread t2 = new Thread(window3);
Thread t3 = new Thread(window3);
t1.start();
t2.start();
t3.start();
}
}
启动main方法可以观察到控制台的输出是符合要求的。
2、处理继承Thread的线程安全
创建类继承Thread
package com.chw.threaddemo.entity;
/**
* @author chw1113
* @create 2022-08-13 9:46
*/
public class Window4 extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
if (ticket < 1) {
break;
}
}
}
//同步方法必须声明为静态的static,使同步锁默认为this.class
private static synchronized void show() {//默认同步锁:Window4.class
if (ticket > 0) {
try {
sleep(100);//用于加大不安全情况出现的概率
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为 : " + ticket);
ticket--;
}
}
}
测试
package com.chw.threaddemo.entity;
public class WindowTest4 {
public static void main(String[] args) {
Window4 t1 = new Window4();
Window4 t2 = new Window4();
Window4 t3 = new Window4();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
启动main方法可以观察到控制台的输出是符合要求的。
3.3 两种方式对比
1、同步方法也涉及到同步监视器,只是不需要显示声明。
2、非静态的同步方法,其同步监视器(同步锁)是:this
3、静态的同步方法,其同步监视器(同步锁)是:this.class
3.4 同步机制应用举例:单例模式-饿汉式
Double Check(双重校验) + Lock(加锁)
public class Singleton {
private static Singleton singleton;
private Singleton(){};
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
解释
- 如果singleton不为空,则直接返回对象,不需要获取锁;而如果多个线程发现singleton为空,则进入分支;
- 多个线程尝试争抢同一个锁,只有一个线程争抢成功,获取到锁的线程会再次判断singleton是否为空,因为singleton有可能已经被上一个拿到锁的线程实例化;
- 其它之后获取到锁的线程在执行到第4行校验代码,发现singleton已经不为空了,则不会再new一个对象,直接返回对象;
- 之后所有进入该方法的线程都不会去获取锁,在第一次判断singleton对象时已经不为空了
两次判空,且使用类对象作为同步锁,故该懒汉式写法也被称为:Double Check(双重校验) + Lock(加锁)
4、线程死锁
死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,形成死锁。
- 死锁不会报异常,不会提示,但所有的线程都处于阻塞状态,无法继续。
解决方法
- 专门的算法、原则;
- 尽量减少同步资源的定义;
- 尽量避免嵌套同步;
5、Lock锁方式解决线程安全问题 (JDK5.0新增)
操作步骤
//1.实例化Lock接口的实现类ReentrantLock
private ReentrantLock lock = new ReentrantLock(true);//true表示轮流给锁,不用看脸抢,默认false
@Override
public void run() {
//用try{}finally{}包住
try {
lock.lock();//2.实现类对象调用lock()方法锁定,手动锁定
....
} finally {
lock.unlock();//解锁,手动解锁
}
}
代码演示
package com.chw.threaddemo.entity;
import java.util.concurrent.locks.ReentrantLock;
public class Window implements Runnable{
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
lock.lock();
if (ticket > 0) {
try {
//用于加大不安全情况出现的概率
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为 : " + ticket);
ticket--;
}
} finally {
lock.unlock();
}
}
}
}
测试
package com.chw.threaddemo.entity;
public class LockTest {
public static void main(String[] args) {
Window window = new Window();
Thread t1 = new Thread(window);
Thread t2 = new Thread(window);
Thread t3 = new Thread(window);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
Lock方式与synchronized方式的对比
- 相同:二者都可以解决线程安全问题
- 不同
- synchronized自动释放同步锁;
- Lock方式要手动启动同步lock(),结束同步也要手动解锁unlock();
6、线程通信例题
用wait( )、notify( )、notifyAll()简单实现两个线程交替打印1-100。
- wait():执行此方法使当前线程进入阻塞,并释放同步锁;
- notify():执行此方法将唤醒被wait()的一个线程,如果有多个线程被wait(),就唤醒优先级最高的一个;
- notifyAll():执行此方法唤醒所有被wait()的线程;
注意
- wait( )、notify( )、notifyAll( ) 必须使用在synchronized中,不能用在Lock方式,因为Lock方式是要手动释放锁的。
- wait( )、notify( )、notifyAll( ) 的调用者必须是同步监视器(锁)。即如果显示声明了同步监视器(锁),则要用这个锁来调用 wait( )、notify( )、notifyAll( ) 。
- wait( )、notify( )、notifyAll( ) 都不属于Thread类,而是属于Object基础类,也就是每个对象都有wait( )、notify( )、notifyAll( ) 的功能,因为每个对象都有锁,锁是每个对象的基础,当然操作锁的方法也是最基础了。
代码演示
创建Runnable实现类
package com.chw.threaddemo.printtest;
public class Number implements Runnable {
private int number = 1;
@Override
public void run() {
while (true) {
synchronized (this) {//同步代码块
//唤醒被wait()的线程
notify();
if (number <= 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " : " + number);
number++;
try {
//阻塞,进入等待唤醒状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
测试
package com.chw.threaddemo.printtest;
/**
* @author chw1113
* @create 2022-08-13 11:26
*/
public class CommunicationTest {
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();
}
}
输出结果符合预期。
经典例题:生产者/消费者
分析
1、有两个线程:生产者线程、消费者线程;
2、共享数据:产品数量;
3、通过同步机制解决线程安全问题;
4、线程之间是否有通信?是
要求生产商品的方法和消费商品的方法这两个行为是互斥的,不能并行,否则商品数据就会出错,所以这两个方法要加锁。
需要4个类
- Producer:实现Runnable接口,用于开启生产者线程
- Consumer:实现Runnable接口,用于开启消费者线程
- Clerk:店员,提供生产商品和消费商品的两个同步方法,包括通信操作
- ProductTest:测试
代码演示
Producer
package com.chw.threaddemo.demo;
public class Producer implements Runnable{//生产者
private Clerk clerk;//用于调用生产商品的同步方法
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " : 开始生产商品");
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produce();//调用生产商品的方法
}
}
}
Consumer
package com.chw.threaddemo.demo;
public class Consumer implements Runnable{//消费者
private Clerk clerk;//用于调用消费商品的同步方法
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " : 开始消费商品");
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consume();//调用消费商品的方法
}
}
}
Clerk
package com.chw.threaddemo.demo;
public class Clerk {
private int productCount = 0; //用于生产消费的共享数据:商品数量
//生产商品
public synchronized void produce() {
if (productCount < 20) {//商品数量小于20才生产
productCount++;
System.out.println(Thread.currentThread().getName() + " : 生产第" + productCount + "个商品");
notify();//生产了商品之后就可以唤醒消费者了
} else {//商品>=20,不需要生产
try {
wait();//商品>=20进入阻塞等待唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费商品
public synchronized void consume() {
if (productCount > 0) {//商品数量大于0才消费
System.out.println(Thread.currentThread().getName() + " : 消费第" + productCount + "个商品");
productCount--;
notify();//消费了商品之后就可以唤醒生产者了
} else {//商品<0,无法消费
try {
wait();//进入阻塞等待唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
ProductTest
package com.chw.threaddemo.demo;
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer consumer = new Consumer(clerk);
Thread p1 = new Thread(producer);
Thread c1 = new Thread(consumer);
p1.setName("生产者1");
c1.setName("消费者1");
p1.start();
c1.start();
}
}
面试题
1、sleep() 和 wait() 的异同?
-
相同
- 一旦执行方法,线程进入阻塞
-
不同
- 声明位置不同,sleep()是在Thread中声明的,wait()是在Object中声明的;
- 调用的要求不同:sleep()可以在任何需要的场景下调用。wait() 必须在同步代码块或同步方法中调用;
- 如果都调用在同步代码块或同步方法中,wait()会释放锁,sleep()不会释放锁;
2、创建多线程有几种方式?
四种
- 继承Thread
- 实现Runnable
- 实现Callable
- 线程池
7、第三种方式创建多线程:实现Callable接口
与使用Runnable相比,Callable功能更强大
- 相比run()方法,可以有返回值;
- 方法可以抛异常;
- 支持泛型的返回值;
- 需要借助FutureTask类,里面有get方法可以获取返回结果;
Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等;
- FutureTask是Future接口唯一实现类;
- FutureTask同时实现了Runnable、Future接口。既可以作为Runnable被线程执行,也可以作为Future得到Callable的返回值;
代码演示
实现Callable接口
package com.chw.threaddemo.calldemo;
import java.util.concurrent.Callable;
public class NumThread implements Callable<Object> {
@Override
public Object call() throws Exception {//重写call方法,注意到有返回值
int sum = 0;
//打印100以内的偶数并求和赋给sum
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println("i = " + i);
sum += i;
}
}
return sum;//返回100以内的偶数和
}
}
测试线程启动
package com.chw.threaddemo.calldemo;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadNew {
public static void main(String[] args) {
NumThread numThread = new NumThread(); //创建实现了Callable的实例
FutureTask futureTask = new FutureTask<>(numThread);//传参给FutureTask的构造器创建FutureTask实例
Thread t1 = new Thread(futureTask);//FutureTask实例传参给Thread构造器创建Thread类对象
t1.start();//调用start()启动线程
try {
//get方法的返回值就是FutureTask构造器参数Callable实现类重写的call()的返回值
Object sum = futureTask.get();
System.out.println("sum = " + sum);//打印返回值
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
启动main方法得到预期结果。
8、第四种方式创建多线程:线程池
线程池相关API
代码演示
package com.chw.threaddemo.pooldemo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
public static void main(String[] args) {
//1. 先提供指定线程数量的线程池
//newFixedThreadPool(n):创建一个可重用固定线程数的线程池
ExecutorService serv = Executors.newFixedThreadPool(10);//创建10个线程数的线程池
ThreadPoolExecutor service = (ThreadPoolExecutor) serv;
//2. 设置线程池的属性
service.setCorePoolSize(15);//核心池大小
service.setKeepAliveTime(1,SECONDS);//setKeepAliveTime(long time, TimeUnit unit)
service.setMaximumPoolSize(30);//最大线程数
//3.执行线程的操作
service.execute(/*Runnable command*/);//执行任务/命令,没有返回值,一般用来执行Runnable
T t = service.submit(/*Callable<T> task*/);//执行任务,有返回值,一般用来执行Callable
//4.关闭线程池
service.shutdown();//关闭线程池
}
}