目录
4.1.4 面试题: synchronized 与Lock的异同?
五、线程的通信——wait、notify、notifyall
6.1.1与使用Runnable相比,Callable功能 更强大些
课程链接:https://www.bilibili.com/video/BV1Kb411W75N?p=415&spm_id_from=pageDriver
IDEA基本操作
修改类头的文档注释信息
一、基本概念:程序、进程、线程
1.1 线程(thread)
进程可进一步细化为线程,是一个程序内部的一条执行路径。
- ➢ 若一个进程同一时间并行执行多个线程,就是支持多线程的
- ➢ 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- ➢ 一个进程中的多个线程共享相同的内存单元/内存地址空间 -》它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
1.2 多核cpu和单核cpu
- ➢ 单核CPU,其实是一种假的多线程,因为在一 个时间单元内,也只能执行一个线程的任务。
- ➢ 一个Java应用程序java.exe, 其实至少有三个线程: main()主线程, gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
1.3 并行与并发
- ➢ 并行: 多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- ➢ 并发: 一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
二、 线程的创建和使用
● Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。
● Thread类的特性
- ➢每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
- ➢通过该Thread对象的star()方法来启动这个线程,而非直接调用run()(start方法的作用:启动线程;调用当前线程的run方法)
2.1 多线程的创建
2.1.1 方式一 : 继承于Thread类
- 1.创建一个继承于Thread类的子类
- 2.重写Thread类的run()
- 3.创建Thread类的子类的对象
- 4.通过此对象调用start()
class MyThread extends Thread {
@override
public void run() {
for(inti=0;i<100;i++){
if(i%2==0){
System.out.println(i);
}
}
}
}
2.1.2 方法二: 实现Runnable接口
- 1.创建一个实现Runnable接口的类
- 2.实现类去实现Runnable中的抽象方法: run()
- 3.创建实现类的对象
- 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 5.通过Thread类的对象调用start()
class MThread implements Runnable{
@Override
public void run() {
for(inti=0;i<100;i++){
if(i%2==0){
System. out . println(i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3.创建实现类的对象
MThread mThread = new MThread( ) ;
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(mThread);
//5.通过Thread类的对象调用start():@启动线程②调用当前线程的run()---》调用了runnable类型的target的run()方法,即代码中mThread的run方法
t1.start();
//再启动一个线程,遍历100以内的偶数
Thread t2 = new Thread(mThread);
t2.start();
}
}
2.1.3 对比两种方法
开发中:优先选择:实现Runnable接口的方式
原因:
- 1.实现的方式没有类的单继承性的局限性
- 2.实现的方式更适合来处理多个线程有共享数据的情况。
联系: public class Thread impl ements Runnable
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
2.2 thread中的常用方法
- 1. start(): 启动当前线程;调用当前线程的run()
- 2. run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
- 3. currentThread(): 静态方法,返回执行当前代码的线程
- 4. getName(): 获取当前线程的名字
- 5. setName():设置当前线程的名字
- 6. yield(): 释放当前cpu的执行权
- 7. join(): 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
- 8. stop(): 已过时。当执行此方法时,强制结束当前线程。
- 9. sleep(long millitime): 让当前线程“睡眠"指定的millitime毫秒。在指定的millitime亳秒时间内,当前线程是阻塞状态。
- 10. isAlive(): 判断线程是否存活
2.3 线程调度
2.3.1 调度策略
- ➢时间片
- ➢抢占式:高优先级的线程抢占CPU
2.3.2 Java的调度方法
- ➢同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- ➢对高优先级,使用优先调度的抢占式策略
2.3.3 线程的优先级
● 线程的优先级等级
- ➢MAX_ PRIORITY: 10
- ➢MIN PRIORITY: 1
- ➢NORM PRIORITY: 5 默认优先级是5
● 涉及的方法
- ➢getPriority() : 返回线程优先值
- ➢setPriority(int newPriority) : 改变线程的优先级
● 说明
- ➢线程创建时继承父线程的优先级
- ➢高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
2.3.4 线程分类
Java中的线程分为两类:一种是守护线程,一种是用户线程。
- 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开
- 守护线程是用来服务用户线程的,通过在start()方法前调用
thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
- Java垃圾回收就是一一个典型的守护线程。
- 若JVM中都是守护线程,当前JVM将退出。
三、线程的生命周期
JDK中用Thread.State类定义了线程的几种状态
- ➢ 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- ➢ 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- ➢ 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
- ➢ 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态|
- ➢ 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
四、线程的同步——用于解决线程安全问题
方法一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:
- 操作共享数据的代码,即为需要被同步的代码。
- 共享数据:多个线程共同操作的变量。比如: ticket 就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。要求:多个线程必须要共用同一把锁。
//如果改成继承Thread的方式,则需要将锁static,否则生成几个线程,就会产生几个obj
public class ticket implements Runnable {
Integer ticket=100;
Object obj = new Object();//充当锁的目的 如果使用继承thread的方式实现多线程,需要static
@Override
public void run() {
while(true){
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (Exception e) {
e. getMessage();
}
ticket--;
} else {
break;
}
}
}
}
}
方法二:同步方法
使用 synchronized 关键字修饰的方法即为同步方法。此时,使用this作为同步锁。
public class ticket implements Runnable {
Integer ticket=100;
@Override
public void run(){
show();
}
private synchronized void show( ) { //同步监视器为this
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
}
}
若使用继承thread的方法实现多线程,则同步方法的实现方式如下:
public class ticket extends Thread {
static Integer ticket=100;
@Override
public void run(){
show();
}
private static synchronized void show( ) { //同步监视器为当前类
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
}
}
关于同步方法的总结:
- 1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 2.非静态的同步方法,同步监视器是: this。静态的同步方法,同步监视器是:当前类本身
4.1 死锁 DeadLock
4.1.1 基本概念
死锁
- ➢不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- ➢出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法
- ➢专门的算法、原则
- ➢尽量减少同步资源的定义
- ➢尽量避免嵌套同步
4.1.2 死锁的例子
public class ticket{
public static void main(){
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run(){
synchronized (s1){
s1.append("a");
s2.append("1");
try{
Thread.sleep(100);
}catch (Exception e){
e.getMessage();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(){
@Override
public void run(){
synchronized (s2){
s1.append("c");
s2.append("3");
try{
Thread.sleep(100);
}catch (Exception e){
e.getMessage();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
}
}
String | StringBuffer | StringBuilder |
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间 | StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 | 可变类,速度更快 |
不可变 | 可变 | 可变 |
线程安全 | 线程不安全 | |
多线程操作字符串 | 单线程操作字符串 |
4.1.3 Lock(锁)解决线程安全问题
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接 口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock 类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
例子
public class ticket implements Runnable{
private int ticket = 100;
private ReentrantLock lock=new ReentrantLock();
@Override
public void run(){
while (true){
try{
lock.lock(); //锁定
if(ticket>0){
try {
Thread.sleep(100);
}catch (Exception e){
e.getMessage();
}
System.out.println(Thread.currentThread().getName());
ticket--;
}else{
break;
}
}finally {
lock.unlock(); //解锁
}
}
}
}
4.1.4 面试题: synchronized 与Lock的异同?
相同:
- 二者都可以解决线程安全问题
不同:
- synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
- Lock需要手动的启动同步(Lock()) ,同时结束同步也需要手动的实现(unlock() )
- Lock 只有代码块锁,synchronized 有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock >同步代码块(已经进入了方法体,分配了相应资源) >同步方法(在方法体之外)
五、线程的通信——wait、notify、notifyall
5.1 基本概念
涉及到的三个方法:
- wait():-一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():-一执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
例子:使用两个线程,打印1-100,交替打印
public class ticket implements Runnable {
private int number = 100;
private ReentrantLock lock = new ReentrantLock();
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (this) {
//唤醒wati中的线程
obj.notify();
if (number <= 100) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.getMessage();
}
number++;
try {
//遇到wait,线程会释放锁,并等待notify唤醒
obj.wait();
} catch (Exception e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
总结:
- 1. wait(),notify(), notifyAll()三个方法必须使用在同步代码块或同步方法中。(也就是synchronized)
- 2. wait(), notify(), notifyAlL()三个方法的调用者必须是同步代码块或同步方法中的同步监视器(例如上面例子中的obj)。否则,会出现ILLegaLMonitorStateException异常
- 3. wait(), notify(), notifyALL()三个方法是定义在java. Long. object类中。
5.2 sleep和wait的异同
1.相同点:
- 一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:
- 1)两个方法声明的位置不同: Thread类中声明sleep(),object 类中声明wait()
- 2)调用的要求不同: sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或者同步方法中使用。
- 3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait() 会释放锁
5.3 生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品
如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一~下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
- ➢生产者比消费者快时,消费者会漏掉-些数据没有取到。
- ➢消费者比生产者快时,消费者会取相同的数据。
分析:
- 1.是否是多线程问题? 是,生产者线程,消费者线程
- 2.是否有共享数据? 是,产品
- 3.如何解决线程的安全问题? 同步机制,有三种方法
- 4.是否涉及线程的通信? 是
class Clerk {
private Integer productCount = 0;
public synchronized void produceProduct() {
if (productCount < 20) {
productCount++;
System.out.println(Thread.currentThread().getName() +
"开始生产第" + productCount + "件产品");
notify(); //唤醒对方去消费
} else {
try {
wait();
}catch (Exception e){
e.printStackTrace();
}
}
}
public synchronized void consumerProduct() {
if (productCount > 0) {
productCount--;
System.out.println(Thread.currentThread().getName() +
"开始消费第" + productCount + "件产品");
notify();//唤醒对方去生产
} else {
try {
wait();
}catch (Exception 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(10);
} catch (Exception 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(10);
} catch (Exception e) {
e.printStackTrace();
}
clerk.consumerProduct();
}
}
}
public class ProductTest {
public static void main() {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
p1.start();
c1.start();
}
}
六、JDK5.0新增线程创建方式
6.1 新增方式一 实现Callable接口
6.1.1与使用Runnable相比,Callable功能 更强大些
- ➢相比run()方法,可以有返回值
- ➢方法可以抛出异常
- ➢支持泛型的返回值
- ➢需要借助FutureTask类, 比如获取返回结果
6.1.2 Future接口
- ➢可以对具体Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。
- ➢Futrue Task是Futrue接口的唯一的实现 朔
- ➢FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
6.1.3 使用示例
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class NumThread implements Callable {
@Override
public Object call() throws Exception {
Integer sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main() {
NumThread numThread = new NumThread();
FutureTask futureTask = new FutureTask(numThread);
new Thread(futureTask).start();
try {
Object num = futureTask.get();//获取call方法的返回值
System.out.println(num);
} catch (Exception e) {
e.printStackTrace();
}
}
}
6.1.4 如何理解callable比runnable强大
- call()可以有返回值的。
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
- Ccallable是支持泛型的
6.2 线程池
6.2.1 背景
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
- ➢提高响应速度(减少了创建新线程的时间)
- ➢降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- ➢便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize: 最大线程数
- keepAliveTime: 线程没有任务时最多保持多长时间后会终止
6.2.2线程池相关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): 创建一个线程池, 它可安排在给定延迟后运行命令或者定期地执行。
例子
import java.util.concurrent.*;
class NumberThread implements Runnable {
@Override
public void run() {
System.out.println("小闹儿要加油!!!");
}
}
public class ThreadPool {
public static void main() {
//提供线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//设置线程池的属性
ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
service1.getKeepAliveTime(TimeUnit.DAYS); //可以通过调用接口,使用线程的功能
//执行线程操作
service.execute(new NumberThread()); //适合使用runnable
//service.submit();//适合适用于callable
//关闭连接处
service.shutdown();
}
}