参考资料:《Java线程》、《JDK API 1.6 中文版》
学习视频:b站《尚硅谷Java入门视频教程》(主讲:宋红康老师)
第九篇 Java多线程
一、基本概念
- 线程是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程
- 每个线程都有一个优先级,高优先级线程的执行优先于低优先级线程。每个线程都可以或不可以标记为一个守护程序。当某个线程中运行的代码创建一个新 Thread对象时,该新线程的初始优先级被设定为创建线程的优先级,并且当且仅当创建线程是守护线程时,新线程才是守护程序。
- 当Java虚拟机启动时,通常都会有单个非守护线程(它通常会调用某个指定类的main方法)。Java虚拟机会继续执行线程,直到下列任一情况出现时为止:
✒️ 调用了Runtime类的exit方法,并且安全管理器允许退出操作发生。
✒️ 非守护线程的所有线程都已停止运行,无论是通过从对 run 方法的调用中返回,还是通过抛出一个传播到run方法之外的异常。 - 创建新执行线程有两种方法。一种方法是将类声明为Thread的子类。该子类应重写Thread类的run方法;另一种方法是声明实现Runnable接口的类。该类然后实现run方法。然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动。
—— 摘自官方文档JDK API 1.6 中文版
1、程序、进程和线程
- 程序(Program):为完成特定任务或功能所编写的一组指令集合,即一段静态的代码/静态对象
- 进程(Process):程序执行的结果,是动态的过程,拥有生命周期,作为系统资源分配的单位。系统在运行时会为其分配不同的内存。
- 线程(Thread):进程的进一步细化,是程序内的一条执行路径。
✒️一个Java程序至少有3个线程:main( )主线程、gc( )垃圾资源回收线程、异常处理线程
✒️Java线程可分为两类:一是守护线程,二是用户线程(java垃圾回收是一个典型的守护线程)
✒️setDaemon( true )可以将一个用户线程变成守护线程,若JVM中都是守护线程,则JVM将退出
2、并行和并发
- 并行:多个CPU同时执行多个任务
- 并发:单个CPU执行多个任务(和串口传输类似,实际上不是同一时刻在执行 —— 时间片)
二、线程的调度
关于线程调度的具体实现和原理,详见《Java线程》第六章
1、线程的调度策略和调度方法
- 时间片+抢占式:**优先级高的线程占用CPU
✒️时间片策略:同优先级线程组成先进先出
队列
✒️抢占式策略:高优先级线程优先
占用CPU
PS:高优先级线程优先抢占CPU执行权,只是抢占的概率高,并不意味着高优先级一定比低优先级的线程先执行
2、线程的优先级
(1)线程优先等级
定义在Thread类里的常量 | 优先级 |
---|---|
MIN_PRIORITY | 1 |
NORM_PRIORITY(默认优先级) | 5 |
MAX_PRIORITY | 10 |
(2)获取和设置线程优先级
- setPriority( int newPriority ):设置线程优先级为newPriority
- getPriority( ):获取线程当前优先级
三、线程的创建和使用
1、方式一:继承于Thread类
(1)使用步骤
- 创建一个子类继承于Thread类
- 重写Thread类的run( )
- 实例化Thread类的子类对象
- 由该对象调用start( )启动线程
package com.javaThread.java;
/* 线程1遍历100内偶数 */
class subThread1 extends Thread{
/* 1、创建子类继承于Thread */
@Override
/* 2、重写run方法 */
/* 3、到main里实例化线程1对象 */
/* 4、到main里用线程1对象调用start方法 */
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0) System.out.println(this.getName()+":"+i);
}
}
}
/* 线程2遍历100内奇数 */
class subThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 != 0) System.out.println(this.getName()+":"+i);
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
subThread1 t1 = new subThread1();
subThread2 t2 = new subThread2();
t1.start();
t2.start();
while (true) {
System.out.println("Hello world");
Thread.sleep(1000);
}
}
}
(2)匿名子类的写法
package com.javaThread.java;
public class ThreadDemo2 {
public static void main(String[] args) {
/* 创建Thread类的匿名子类 */
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0) System.out.println(this.getName()+":"+i);
}
}
}.start();
/* 创建Thread类的匿名子类 */
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 != 0) System.out.println(this.getName()+":"+i);
}
}
}.start();
}
}
(3)Thread类常用方法
- start( ):启动当前线程,会调用当前线程的run( )方法
- run( ):创建线程要执行的操作声明在此方法中(类似Linux多线程中传入pthread_create( )中的指针函数)
- currentThread( ):静态方法,返回当前线程
- getName( ):获取当前线程名
- setName( ):设置当前线程名
- yield( ):释放CPU执行权,重新调度(释放后可能被其他线程抢占,也可能又被自己抢到)
- join( ):阻塞等待其他线程执行完,再结束阻塞
- sleep( long millis ):线程阻塞等待一定时间(毫秒级别)
- isAlive( ):判断线程死活,返回线程状态true/false
- 其他方法(Deprecated - 已过时):stop( )强制结束
(4)经典例子:多窗口售票v1.0
/* 三个窗口(线程)同时卖票,一共100张票 */
class Windows extends Thread{
public Windows(String threadName){
super(threadName);
}
private static int tickets = 100;
@Override
public void run() {
while (tickets > 0){
System.out.println(this.getName()+" > NO."+tickets+" has been sold");
tickets --;
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Windows w1 = new SellTickets("windows_1");
Windows w2 = new SellTickets("windows_2");
Windows w3 = new SellTickets("windows_3");
w1.start();
w2.start();
w3.start();
}
}
- BUG记录:v1.0中存在线程安全问题(重票或者错票)
2、方式二:实现Runnable接口
- 说明:在官方API文档中写着创建线程有两种方式,一是继承于Thread类,二是实现Runnable接口。实际上,在JDK 5.0后又新增的另外两种方式,实现Callable接口和使用线程池的方式。(一共就有四种创建线程的方式)
(1)使用步骤
- 创建一个实现Runnable接口的类(implements)
- 实现Runnable中的抽象方法run( )
- 创建实现类的对象
- 将此对象传到Thread类的构造器中并创建Thread对象
- 通过Thread对象去调用start( )方法
/* 创建线程方式二:implements Runnable的方式*/
/* 1.创建一个实现Runnable接口的类(implements) */
class Thread1 implements Runnable{
/* 2.实现Runnable中的抽象方法run() */
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0) System.out.println(i);
}
}
}
public class ThreadDemo4 {
public static void main(String[] args) {
/* 3.创建实现类的对象 */
Thread1 t1 = new Thread1();
/* 4.将此对象传到Thread类的构造器中并创建Thread对象 */
Thread thread1 = new Thread(t1);
//Thread thread1 = new Thread(new Thread1()); //3+4
/* 5.通过Thread对象去调用start()方法 */
thread1.start();
}
}
(2)经典例子:多窗口售票v1.1
/* 实现Runnable接口的方式创建的线程 */
class Windows implements Runnable{
private int tickets = 100; //和v1.0不同,这里不用static
@Override
public void run() {
while(tickets > 0){
System.out.println(Thread.currentThread().getName()+"> NO."+tickets+" has been sold");
tickets --;
}
}
}
public class ThreadDemo5 {
public static void main(String[] args) {
Windows windows = new Windows();
/* 一个windows可传到三个Thread类的构造器中 */
Thread t1 = new Thread(windows);
Thread t2 = new Thread(windows);
Thread t3 = new Thread(windows);
/* 共用一个windows对象,也就共享100张票 */
t1.setName("window_1");
t2.setName("window_2");
t3.setName("window_3");
t1.start();
t2.start();
t3.start();
}
}
/* 同样这里也有线程安全的问题,出现了重票的错票的问题,待解决 */
3、两种创建线程方式的对比
- 开发中,优先考虑用实现Runnable接口的方式创建线程,原因如下:
✒️实现的方式没有类的单继承性的局限性
✒️实现的方式更适合处理多线程操作共享数据的情况 - 联系:实际上,Thread类中也实现的Runnable接口
- 相同点:两种方式都需要重写run( )方法,将线程执行的逻辑声明在run( )中
四、线程的生命周期
- 其中,suspend( )、resume( )都是Deprecated - 已过时的,因为该方法可能会产生死锁。如果某一线程suspend挂起时持有锁,则在该线程重新开始之前任何线程都不能访问该资源。如果重新开始此线程的另一线程调用resume之前锁定该监视器,则会发生死锁。这类死锁通常会证明自己是“冻结”的进程。
五、线程的同步
1、多窗口售票的BUG
- 重票、错票的产生:当某一个线程在操作tickets时,由于中间的时间差(可能极小却不可忽略)或者线程状态变化等问题,操作尚未完成就有其他线程也操作tickets。
(PS:例如当某一个线程拿到tickets=100,还未到tickets--就有其他线程参与进来,这时也拿到了tickets=100。最终出现票号为99的重票;同样的当某一个线程操作完tickets--到等于0,还未结束循环时,另一个线程参与进来,拿到tickets=0就会出现票号为-1的错票)
- 在Java中,通过以下两种同步方式来解决线程安全问题
2、方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
…… …… ……
}
- 操作共享数据的代码,即为需要被同步的代码
- 共享数据:多个线程共同操作的变量。如:窗口售票中的tickets
- 同步监视器(锁):任何一个类的对象都可以充当同步监视器。要求多个线程必须共用同一把锁
✒️实现Runnable接口的方式创建线程,可以考虑用this充当同步监视器
✒️继承Thread类的方式创建线程,可以考虑用当前类作同步监视器,总之需要保证锁是唯一的 - 局限性:同步虽然解决的线程安全问题,但是在操作同步代码时只能有一个线程参与,其余等待,相当于此过程是单线程的。
3、方式二:同步方法
- 同步方法仍然涉及到同步监视器,只是不需要显式的声明
- 静态同步方法,同步监视器是:this
- 非静态同步方法,同步监视器是:当前类
4、线程同步解决窗口售票的BUG
(1)经典例子:多窗口售票v2.0.0
/* 实现Runnable接口的方式创建的线程 */
/* 同步代码块解决线程安全问题 */
class Windows implements Runnable{
private int tickets = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
//可传this,此时的this是windows的唯一对象
/* 为了出现重票和错票的概率变大,这里加入sleep() */
try{
Thread.sleep(50);
}catch (InterruptedException e) {
e.printStackTrace();
}
if(tickets > 0) {
System.out.println(Thread.currentThread().getName() + "> NO." + tickets + " has been sold");
tickets --;
}else{
break;
}
}
}
}
}
public class ThreadDemo5 {
public static void main(String[] args) {
Windows windows = new Windows();
Thread t1 = new Thread(windows);
Thread t2 = new Thread(windows);
Thread t3 = new Thread(windows);
t1.setName("window_1");
t2.setName("window_2");
t3.setName("window_3");
t1.start();
t2.start();
t3.start();
}
}
(2)经典例子:多窗口售票v2.0.1
/* 继承Thread类的方式创建的线程 */
/* 同步方法解决线程安全问题 */
class Windows extends Thread {
public Windows(String threadName) {
super(threadName);
}
private static int tickets = 100;
private static boolean flag = true;//标志位,控制循环
@Override
public void run() {
while(flag) {
sellTickets(); //调用同步方法
}
}
/* 同步方法实现,注意是静态的同步方法 */
private static synchronized void sellTickets() {
/* sleep提高错票重票的概率 */
try{
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " > NO." + tickets + " has been sold");
tickets--;
}else{
flag = false;
}
}
}
public class ThreadDemo6 {
public static void main(String[] args) {
Windows w1 = new Windows("windows_1");
Windows w2 = new Windows("windows_2");
Windows w3 = new Windows("windows_3");
w1.start();
w2.start();
w3.start();
}
}
5、线程死锁问题
- 死锁:不同的线程分别占用对方的同步资源,都在等待对方释放自己所需的同步资源,造成了线程的死锁。
✒️出现死锁后不会报错和抛异常,所有线程处在阻塞状态,无法继续。
✒️避免嵌套同步、尽量减少同步资源的定义以避免出现死锁
/* 死锁的简单例子 */
public class ThreadDemo6 {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
/* 线程1 - 匿名类*/
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
/* sleep增加死锁概率 */
try{
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
/* 当线程1先执行,拿到s1阻塞,此时线程2可能刚好拿了s2也阻塞,
* 接着就出现线程1拿着s1等s2,线程2拿着s2等s1 */
/* 线程2 */
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
/* sleep增加死锁概率 */
try{
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
6、方式三:Lock锁(JDK 5.0新增)
- 问:synchronized和Lock的异同?
答:虽然synchronized和Lock都能解决线程安全的问题,但是使用synchronized的方式在执行完相应的同步代码后,即使发生异常,也会自动释放占用的锁(同步监视器)。而用Lock的方式则需要手动的启动(lock( ) -上锁),手动的结束同步(unlock( ) - 解锁),在发生异常时若没有unlock( ),很有可能导致死锁,所以unlock( )需要写在finally中以保证异常情况下锁也能够被释放;且synchronized是非公平锁,Lock可以选择公平或非公平锁。(默认非公平) - 用法见下例代码:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Window implements Runnable{ private int tickets = 100; /* 实例化ReentrantLock对象 */ ReentrantLock l = new ReentrantLock(); @Override public void run() { while(true){ /* 给同步代码上锁 */ l.lock(); try{ if(tickets > 0){ try{ Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+">\tNO."+tickets+" has been sold"); tickets --; }else break; }finally { /* 手动解锁 */ l.unlock(); } } } } public class LockTest{ public static void main(String[] args) { Window w = new Window(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("window 1");t1.start(); t2.setName("window 2");t2.start(); t3.setName("window 3");t3.start(); } }
7、编程练习
银行有一个账户,有两个储户分别向同一个账户存3000元,每次存1000,存5次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
【提示】
1,明确哪些代码是多线程运行代码,须写入run()方法
2,明确什么是共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。
【拓展问题】能否实现储户交替存钱的操作?
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
class Account{
//账号
private double balance = 0; //当前余额
/* 存钱并显示当前余额的方法,操作共享数据需要同步 */
public synchronized void saveMoney(double money){
if(money < 0) return;
balance = balance + money;
String customerName = Thread.currentThread().getName();
System.out.println(customerName+"> "+"Deposit "+money+"¥ succeed.Your current balance is "+balance+"¥");
}
}
class Customer extends Thread{
//客户——线程
private Account acc;
private ReentrantLock lock = new ReentrantLock();
/* 提供构造器传入账户,可实现多客户共用一个账户 */
public Customer(Account account) {
this.acc = account;
}
/* 重写run方法,根据题目 */
@Override
public void run() {
for(int i=0;i<5;i++){
/* 提高出现存钱问题概率,加入睡眠 */
try{
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
acc.saveMoney(1000);
}
}
}
public class AccountTest{
public static void main(String[] args) {
Account acc = new Account();
Customer c1 = new Customer(acc);
Customer c2 = new Customer(acc);
Customer c3 = new Customer(acc);
c1.setName("Customer 1");
c2.setName("Customer 2");
c1.start();
c2.start();
}
}
六、线程间通信
- wait( ):调用此方法当前线程进入阻塞状态,并释放同步监视器
- notify( )、notifyAll( ):调用此方法会唤醒正在处于wait的线程,多个线程则唤醒优先级高的线程
(PS:以上只能在同步代码块或者同步方法中使用-sychronized,且其调用者必须是同步监视器,否则会出现IllegalMonitorStateException) - 【拓展问题】实现两个储户交替存钱的操作?
import java.util.concurrent.locks.ReentrantLock;
class Account {
//账号
public double balance = 0; //当前余额
/* 存钱并显示当前余额的方法,操作共享数据需要同步 */
static ReentrantLock l = new ReentrantLock();
public synchronized void saveMoney(double money) {
if (money < 0) {
System.out.println("error");
return;
}
else {
notify();
balance = balance + money;
String customerName = Thread.currentThread().getName();
System.out.println(customerName + "> " + "Deposit " + money + "$ succeed.Your current balance is " + balance + "$");
try {
wait(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Customer extends Thread {
//客户——线程
private Account acc;
/* 提供构造器传入账户,可实现多客户共用一个账户 */
public Customer(Account account) {
this.acc = account;
}
/* 重写run方法,根据题目 */
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
acc.saveMoney(1000);
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account acc = new Account();
Customer c1 = new Customer(acc);
Customer c2 = new Customer(acc);
c1.setName("Customer 1");
c2.setName("Customer 2");
c1.start();
c2.start();
}
}
七、创建线程另外两种方式(JDK 5.0新增)
1、实现Callable接口
- 与Runnable相比,Callable的功能更加强大:
✒️可以有返回值
✒️可以抛出异常
✒️支持泛型
✒️可借助FutureTask类,比如获取返回结果 - 使用步骤:
✒️创建一个实现Callable的实现类
✒️重写call方法,将线程执行的操作声明在call( )中
✒️new一个Callable接口实现类的对象传到FutureTask构造器中
✒️new一个FutureTask对象传到Thread类的构造器中
✒️使用Thread类对象调用start( )启动线程 - 下面使用实现Callable接口的方式创建线程,依然是上面"储户存钱"的问题
package com.ThreadDemos.java;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class Account{
public double balance = 0;
}
/* 1.创建一个实现Callable的实现类 */
class Customer implements Callable {
static Account acc = new Account();
/* 2.重写call方法,将线程执行的操作声明在call()中 */
@Override
public Object call() throws Exception {
String threadName = Thread.currentThread().getName();
for (int i = 0; i < 3; i++) {
synchronized (acc) {
Thread.sleep(1000);
acc.balance += 1000;
System.out.println(threadName + "转入金额:"+1000+"$");
}
}
return acc.balance;
}
}
public class CallableTest {
public static void main(String[] args) {
Customer customer1 = new Customer();
Customer customer2 = new Customer();
/* 3.new一个Callable接口实现类的对象传到FutureTask构造器中 */
FutureTask futureTask1 = new FutureTask(customer1);
FutureTask futureTask2 = new FutureTask(customer2);
/* 4.new一个FutureTask对象传到Thread类的构造器中 */
Thread t1 = new Thread(futureTask1);
Thread t2 = new Thread(futureTask2);
/* 5.使用Thread类对象调用start()启动线程 */
t1.setName("客户1");
t2.setName("客户2");
t1.start();
t2.start();
try {
if((double)futureTask1.get() > (double)futureTask2.get()){
System.out.println("当前账户余额是:"+futureTask1.get()+"$");
}else {
System.out.println("当前账户余额是:"+futureTask2.get()+"$");
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
2、使用线程池创建
- 概述:提前创建好多个线程组成线程池,使用时从线程池中直接获取,使用完重新放回池中。可以避免频繁创建、销毁的操作,实现重复利用;对于创建和销毁、使用线程量大的资源(如:高并发线程),能提高响应速度、降低资源销毁。
- 优点:①减少了创建线程的时间,提高响应速度;②重复利用线程池中的线程,降低了资源消耗;③便于线程管理
- 使用步骤:
✒️提供指定线程数量的线程池
✒️提供实现Runnable接口或者Callable接口实现类对象
✒️关闭线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPollTest {
public static void main(String[] args) {
/* 1.提供指定线程数量的线程池 */
ExecutorService service = Executors.newFixedThreadPool(10);
/* 2.设置线程池的属性[可选] */
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) service;
threadPoolExecutor.setCorePoolSize(10); //设置核心池大小
threadPoolExecutor.setMaximumPoolSize(10); //设置最大线程数
/* 3.提供实现Runnable接口或者Callable接口实现类对象 */
service.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i%2 == 0) System.out.print(i+" ");
}
}
});
/* 4.关闭线程池 */
service.shutdown();
}
}