多线程
- 1、 基本概念 程序、进程、线程
- 2、线程的创建和使用(重点)
-
- 2.1、Thread类
- 2.2、Thread类中有关的基本方法
- 2.3、线程调度 时间片式 抢占式等
- 2.4、JDK5.0之前创建线程的两种方式
-
- 2.41方式一:继承Thread类
- 2.42方式二:实现Runnable接口
- 2.43、比较创建线程的两种方式
- 2.5、jDK5.0新增创建线程的两种方式
-
- 2.51、方式一:实现Callable接口
- 2.52、方式二:使用线程池(主要使用该方式)
- 2.53、如何理解实现callable接口方式比实现runnable接口方式更为强大
- 3、线程的生命周期
- 4、线程的同步(重点)
-
- 4.1、线程安全问题
- 4.2、同步机制 解决线程安全问题
-
- 4.21、机制一:同步代码块
- 4.22、机制二:同步方法
- 4.23、机制三 Lock(锁) 同步锁
- 4.3、线程死锁问题
- 4.4、synchronized 与 Lock 的对比
- 5、线程的通信
-
- 5.1、三方法:wait() 与 notify() 和 notifyAll()
- 5.2、sleep()与wait()的异同(面试)
- 5.3、经典例题:生产者与消费者问题
1、 基本概念 程序、进程、线程
- 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码,静态对象。
- 进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
程序是静态的,进程是动态的
- 线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的
线程作为调度和执行的单位,每个线程拥有独立的运行栈(虚拟机栈)和程序计数器(pc),线程切换的开销小
一个进程中的多个线程共享相同的内存单元/内存地址空间(堆/方法区)>>>它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资 源可能就会带来安全的隐患(通过线程的同步来解决)。
一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc() 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
- 单核CPU与多核CPU
一般来讲,线程和核数是1:1的关系(四核CPU一般拥有四个线程),增加核心数目就是为了增加线程数。Intel引入超线程技术,使核心数与线程数形成1:2的关系,如四核Core i7支持八线程(或叫作八个逻辑核心),大幅提升了其多任务、多线程性能。
CPU主频:CPU运算时的工作频率,在单核时间它是决定CPU性能的重要指标,一般以MHz和GHz位单位,高频率能有效提高CPU性能。
单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
多核CPU,一个时间单元内,能执行多个线程的任务。(现在的服务器都是多线程的)
- 并行与并发
并行(parallelism):是同一时刻,多个线程都在执行。(多个CPU同时执行多个线程)
并发(concurrency):是同一时刻,只有一个执行,但是一个时间段内,多个线程都执行了。
- 多线程优点
单核CPU时,只使用单线程先后完成多个任务,比使用多线程来完成时间快。
但是多线程程序可以增加用户体验(对图形化界面来说):听歌和查资料同时进行
- 何时需要多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写 操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
2、线程的创建和使用(重点)
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread 类来体现。
2.1、Thread类
1.特性
①:每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常 把run()方法的主体称为线程体
②:通过该Thread对象的**start()**方法来启动这个线程,而非直接调用run()
2.构造器
①:Thread():创建新的Thread对象
②:Thread(String threadname):创建线程并指定线程实例名
③:Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接 口中的run方法
④:Thread(Runnable target, String name):创建新的Thread对象
2.2、Thread类中有关的基本方法
- void start(): 启动线程,并执行当前线程的run()
- run(): 线程在被调度时执行的操作被声明在此方法中
- String getName(): 返回线程的名称
- void setName(String name):设置该线程名称
- static Thread currentThread(): 返回当前线程。在Thread子类中是this,通常用于主线程和Runnable实现类
- static void yield():线程让步(释放当前CPU的执行权) ①暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 ②若队列中没有同优先级的线程,忽略此方法
- join() :当某个程序执行流(线程A)中调用其他线程(线程B)的 join() 方法时,调用线程(线程A)将被阻塞,直到 join() 方法加入的 join 线程(线程B)执行完为止 注:低优先级的线程也可以获得执行
- static void sleep(long millis):(指定时间:毫秒) ①令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。 ②抛出InterruptedException异常
- stop(): 强制线程生命期结束,不推荐使用 (API中已过时)
- boolean isAlive():返回boolean,判断线程是否还活着
2.3、线程调度 时间片式 抢占式等
线程优先级等级(定义在Thread类中的三个常量)
MAX_PRIORITY:10 MIN _PRIORITY:1 NORM_PRIORITY:5 (默认优先级)
- getPriority() :返回线程优先值
- setPriority(int newPriority) :改变线程的优先级
说明:①线程创建时继承父线程的优先级
②低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
2.4、JDK5.0之前创建线程的两种方式
2.41方式一:继承Thread类
/**
* 多线程的创建 方式一:继承于Thread类
* 1.创建一个继承与Thread的子类
* 2.重写Thread类中的run() -->将此线程执行的操作声明在run()中
* 3.创建Thread类中的子类的对象
* 4.通过此对象调用start()
* <p>
* <p>
* 例:遍历100以内的偶数
*
* @author zck
*/
// 1.创建一个继承与Thread的子类
class MyThread extends Thread {
//2.重写Thread类中的run() -->将此线程执行的操作声明在run()中
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3.创建Thread类中的子类的对象
MyThread myThread1 = new MyThread();
//4.通过此对象调用start():作用①:启动当前线程 ②:调用当前线程的run()
myThread1.start();
//主线程遍历100以内的偶数
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i + "******主线程*******");
}
}
//在创建一个线程,遍历100以内的偶数
MyThread myThread2 = new MyThread();
myThread2.start();
}
}
注意:一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上 的异常“IllegalThreadStateException”。
/**使用匿名子类的方式,实现多个分线程(只执行一次)
* 例:一个线程遍历100以内奇数,一个线程遍历100以内偶数
* @author zck
*/
public class ThreadDemo1 {
public static void main(String[] args) {
//线程一
new Thread(){
@Override
public void run() {
for(int i = 0;i < 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
//线程二
new Thread(){
@Override
public void run() {
for(int i = 0;i < 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
}
2.42方式二:实现Runnable接口
package com.zck.threadtest;
/**
* 多线程的创建 方式二:实现Runnable接口
* 1.创建一个实现Runnable接口的类
* 2.实现类去实现Runnable接口中的抽象方法:run()
* 3.创建实现类的对象
* 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
* 5.通过Thread类的对象调用start()
*
* @author zck
* @create
*/
//1.创建一个实现Runnable接口的类
class MyThread1 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 ThreadTest1 {
public static void main(String[] args) {
//3.创建实现类的对象
MyThread1 myT1 = new MyThread1();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread thread = new Thread(myT1);//target :myT1
//5.通过Thread类的对象调用start():调用当前线程的run()-->调用了runnable类型的target的run()
thread.start();
//再启动一个线程,遍历100以内的偶数
Thread thread1 = new Thread(myT1);//target :myT1
thread1.start();
}
}
2.43、比较创建线程的两种方式
- 开发中优先使用 实现Runnable接口 方式
-
原因:避免了单继承的局限性(面向接口编程的优点)
多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
例:使用共享变量>>不用声明为static
2.5、jDK5.0新增创建线程的两种方式
2.51、方式一:实现Callable接口
package com.zck.threadtest;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**创建线程的方式三:实现callable接口
* 1.创建Callable接口的实现类
* 2.重写实现类中的call()
* 3.创建callable接口实现类的对象
* 4.将callable接口实现类的对象作为参数传递到FutureTask构造器中,并创建FutureTask的对象
* 5.将FutureTask的对象作为参数传递到Thread构造器中,并创建Thread对象,调用start()
* 6.若有需求可获取callcable实现类中call()的返回值 FutureTask的对象.get()
* @author zck
* @create 2020-04-06 13:19
*/
//1.创建Callable接口的实现类
class TestThread implements Callable{
//2.重写实现类中的call()
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1;i <=100;i++){
if (i % 2 ==0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
/*
遍历100以内的偶数,并输出综合
*/
public class ThreadTest5 {
public static void main(String[] args) {
//3.创建callable接口实现类的对象
TestThread testThread = new TestThread();
//4.将callable接口实现类的对象作为参数传递到FutureTask构造器中,并创建FutureTask的对象
FutureTask futureTask = new FutureTask(testThread);
// 5.将FutureTask的对象作为参数传递到Thread构造器中,并创建Thread对象,调用start()
new Thread(futureTask).start();
try {
//6.若有需求可获取callcable实现类中call()的返回值 FutureTask的对象.get()
//get()返回值即为 FutureTask构造器参数(callable实现类:TestThread) 重写的call()的返回值
Object Sum = futureTask.get();
System.out.println("总和为:" + Sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.52、方式二:使用线程池(主要使用该方式)
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
优点:①提高响应速度(减少了创建新线程的时间)
②降低资源消耗(重复利用线程池中线程,不需要每次都创建)
③便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
package com.zck.threadtest;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 创建线程的方式四:使用线程池
* 1.创建制定线程数的线程池 Executors.newFixedThreadPool(线程数) 的方式
* 2.执行制定线程的操作,需要提供实现runnable接口的对象,或callable接口的对象
* 3.关闭连接池 连接池.shutdown();
* @author zck
* @create 2020-04-06 14:48
*/
class testThreadPool implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0)
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
class testThreadPool1 implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0)
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1.创建制定线程数的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//设置线程池的属性(ThreadPoolExecutor为ExecutorService的实现类)
// ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
// service1.setCorePoolSize(15);
//2.执行制定线程的操作,需要提供实现runnable接口的对象,或callable接口的对象
service.execute(new testThreadPool());//适合于runnable接口 new testThreadPool()
service.execute(new testThreadPool1());//适合于runnable接口 new testThreadPool()
// service.submit(Callable callable);//适用于callable接口
//3.关闭连接池
service.shutdown();
}
}
2.53、如何理解实现callable接口方式比实现runnable接口方式更为强大
- call()可有有返回值,run()没有返回值
- call()可以抛出异常,被外部操作所捕获,获取异常信息
- Callable接口支持泛型,Runnable接口不支持
3、线程的生命周期
JDK中用Thread.State类定义了线程的几种状态:
一个线程完整的生命周期通常要经历以下五个阶段
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
4、线程的同步(重点)
4.1、线程安全问题
原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
package com.zck.threadtest;
/**
* 创建三个窗口。买100张票 :使用 实现runnable接口
* 存在线程安全问题
*
* @author zck
* @create
*/
class Window3 implements Runnable {
private int ticket = 100;//不需要static 只创建了一个对象,三个线程共用一个对象
@Override
public void run() {
while (true) {
if (ticket > 0) {
//在这被阻塞 易出现错票(0 、-1号票)
//出现错票的原因: 当只剩1张票时,三个线程都可能获取到该票 ,获取后被阻塞,后相继执行
try {
Thread.sleep(100);//阻塞0.1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":票号为:" + ticket);
//如果在这被阻塞 易出现重票
ticket--;
} else {
break;
}
}
}
}
public class ThreadDemo2_2 {
public static void main(String[] args) {
Window3 window = new Window3();//只创建了一个对象
//三个线程
Thread thread1 = new Thread(window);
Thread thread2 = new Thread(window);
Thread thread3 = new Thread(window);
thread1.start();
thread2.start();
thread3.start();
}
}
解决办法: 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。Java中,通过同步机制解决线程安全问题
4.2、同步机制 解决线程安全问题
Synchronized的使用方法 Synchronized:(中文翻译)已同步
4.21、机制一:同步代码块
synchronized(同步监视器){
//需要被同步的代码:操作共享数据的代码
}
同步监视器:俗称 锁。任何一个类的对象都可以充当同步监视器
要求:多个线程必须要共用同一把锁(即要求解决继承Thread类的方式执行的线程安全问题时,同步监视器(一个对象)必须是静态的)
补充:实现runnable接口方式中 锁可用this 继承Thread类的方式中,锁慎用this 可以为 当前类.class
注意:该机制可以解决线程安全问题,但操作同步代码块时,只能有一个进程参与,其他线程等待——>相当于一个单线程的过程,效率较低
4.22、机制二:同步方法
public synchronized void show (String name){
//需要被同步的代码
}
同步方法的监视器不需要显示的声明,存在默认锁
注:同步方法的锁:静态方法(类名.class)、非静态方法(this)
同步代码块:自己指定,很多时候也是指定为this或类名.class
4.23、机制三 Lock(锁) 同步锁
jdk5.0之后
通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
ReentrantLock 类实现了 Lock
package com.zck.threadtest;
import java.util.concurrent.locks.ReentrantLock;
/**使用同步锁的方式解决线程安全
* @author zck
* @create 2020-04-05 16:13
*/
class Window5 implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true){
try {
//2.调用lock()
lock.lock();
if (ticket > 0 ){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket --;
}else{
break;
}
}finally {
//3.调用解锁方法
lock.unlock();
}
}
}
}
public class ThreadTest3 {
public static void main(String[] args) {
Window5 window5 = new Window5();
Thread t1 = new Thread(window5);
Thread t2 = new Thread(window5);
Thread t3 = new Thread(window5);
t1.setName("线程一");
t2.setName("线程二");
t3.setName("线程三");
t1.start();
t2.start();
t3.start();
}
}
4.3、线程死锁问题
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
注:出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于 阻塞状态,无法继续
例:
package com.zck.threadtest;
/**演示线程死锁问题
* @author zck
* @create
*/
public class ThreadTest2 {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append(1);
//s1阻塞 致使下面一个进程执行概率大大增加 后有阻塞
//导致s1想调用s2 s2想调用s1 出现死锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s2.append(2);
s1.append("b");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append(3);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s2.append(4);
s1.append("d");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
4.4、synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是 隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有 更好的扩展性(提供更多的子类)
5、线程的通信
5.1、三方法:wait() 与 notify() 和 notifyAll()
-
wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队
等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
- notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待 。
- notifyAll ():唤醒正在排队等待资源的所有线程结束等待。
注意:这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报 java.lang.IllegalMonitorStateException 异常。
即三方法的调用者必须是同步代码块或同步方法的同步监视器
package com.zck.threadtest;
/**
* 线程通信举例:打印1--100,使用两个线程交替进行
*
* @author zck
* @create
*/
class Print implements Runnable {
private int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
notify();//唤醒阻塞的线程
if (num <= 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
try {
wait();//使得进来的线程阻塞 并释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class ThreadTest4 {
public static void main(String[] args) {
Print print = new Print();
Thread thread1 = new Thread(print);
Thread thread2 = new Thread(print);
thread1.setName("线程一");
thread2.setName("线程二");
thread1.start();
thread2.start();
}
}
5.2、sleep()与wait()的异同(面试)
- 同:一旦执行方法,都会使线程进入阻塞状态
-
异:①两方法声明位置不同:sleep()声明在Thread类中,wait()声明在Objec类中
②调用的要求不同:sleep()可以在任何需要的场景下被调用,wait()必须在synchronized方法或synchronized代码块中被调用
③:若两方法都使用在同步代码块或同步方法中,sleep()不会释放同步监视器,wait()会释放同步监视器
5.3、经典例题:生产者与消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题: ①生产者比消费者快时,消费者会漏掉一些数据没有取到。 ②消费者比生产者快时,消费者会取相同的数据。
解决方法一:
package com.zck.threadtest;
/**
* 生产者/消费者问题。
*
* @author zck
* @create
*/
class Clerk {//店员
private int productCount = 0;
//生产产品
public synchronized void productorProduct() {
if (productCount < 20) {
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void customersProduct() {
if (productCount > 0) {
System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
productCount--;
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Productor implements Runnable {//生产者
//共用Clerk类
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("产品不足,正在生产。。。。。。");
while (true) {//循环生产
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.productorProduct();
}
}
}
class Customers implements Runnable {
private Clerk clerk;
public Customers(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("顾客正在消费产品。。。");
while (true) {//循环消费
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.customersProduct();
}
}
}
public class ProductorCustomer {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Customers customers = new Customers(clerk);
Thread prod = new Thread(productor);
Thread cust = new Thread(customers);
Thread cust1 = new Thread(customers);
prod.setName("生产者1");
cust.setName("消费者1");
cust1.setName("消费者2");
prod.start();
cust.start();
cust1.start();
}
}
注:本文章是根据哔哩哔哩公开课 Java -Java 学习- Java 基础到高级-宋红康-零基础自学Java-尚硅谷 整理所得
大爱康师傅!!!