多线程
为什么世间美好都与我擦肩而过…
学习过程中持续修改…
一、简介
1. 基本概念
- 程序:是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象
- 进程:是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程;有自身的产生、存在、消亡的过程。
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
- 线程:进程可以进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,程序切换的开销
- 一个进程中的多个线程共享相同的单元/内存地址空间–>他们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能会带来安全的隐患
- 并行和并发
- 并行:多个CPU同时执行多个任务
- 并发:一个CPU(采用时间片)同时执行多个任务
二、线程的基本创建和方法
1. 继承Thread类
/**
* 多线程:
* 创建方式一:
* 1. 继承Thread类
* 2. 重写run()方法
* 3. 创建继承Thread类的对象
* 4. 通过此对象调用start()方法:作用:启动该线程、调用run()方法
*/
public class Test01Thread01 {
public static void main(String[] args) {
//3.创建继承Thread类的对象
ThreadTest t1 = new ThreadTest();
//4.调用start()方法启动线程.
//start():① 启动该线程 ②调用run()方法
t1.start();
}
}
//1.继承Thread类
class ThreadTest extends Thread{
//2.重写run()方法
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0){
System.out.println(i);
}
}
}
}
2. 实现Runnable接口
/**
* 创建多线程方式二:
* 1. 创建实现Runnable接口的类
* 2. 实现类去实现run()方法
* 3. 创建实现类对象
* 4. 将此对象作为参数,传递到Thread类的构造器中,创建Thread对象
* 5. 通过Thread类的对象调用start()
*/
public class Test02Thread02 {
public static void main(String[] args) {
ThreadTest02 t1 = new ThreadTest02();
/*
* 明明调用的时Thread的run()方法,为何执行ThreadTest02的run()方法呢?
* 答:因为Thread的构造器有形参Runnable target(即t1)
* 而在Thread的run()方法中有判断,如果target不为空,
* 则执行target(即t1)
* */
new Thread(t1).start();
//在启动一个线程
new Thread(t1).start();
}
}
class ThreadTest02 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
3. Thread常用方法
- start():启动线程并调用run()方法
- run():线程的操作和功能
- currentThread():静态方法,返回当前线程
- setName():设置线程名字
- getName():获得线程名字
- yield():释放当前cpu的执行,进入就绪状态
- join():在线程a中调用线程b的join()方法,线程a进入阻塞状态,直到线程b完全执行完毕,a结束阻塞
- stop():启用方法,强制结束线程
- sleep(long milliteme):静态方法,让当前线程阻塞指定毫秒
- isAlive():判断当前线程是否存活
4. 两种方式对比
优先选择实现Runnable接口的方式
- 实现方式没有单继承的局限性
- 实现方式更适合来处理多个线程共享数据的情况
联系
- Runnable接口
- 都需要重写run()方法
三、线程的调度
1. 调度策略
- 时间片
- 抢占式:高优先级的线程抢占CPU
2. JAVA调度方法
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度抢占式策略
3. 线程的优先级
- 1)优先级等级
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
- 2)涉及的方法
getPriority()
:返回线程优先值setPriority(int Priority
):设置优先级
四、生命周期
Thread中的State类(枚举)定义类线程的几种状态
1. 新建
当一个Thread类或其子类对象被声明并创建时
2. 就绪
start()后,处于进入线程队列等待CPU时间片。此时已经具备了运行条件,只是没分配到CPU资源
3. 运行
当就绪线程被调度并获得CPU资源时,便进入了运行状态,run()方法定义了线程的操作和功能
4. 阻塞
在某种特殊情况下,被人挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
5. 死亡
完成工作或被强制终止或出现异常
五、线程安全
0. 问题引出
① 何时出现线程安全问题:
- 资源共享
- 执行的不确定性引起结果不确定性
② 问题出现原因:
- 当某个线程操作车票的过程中,尚未完成操作时,其他线程参与进来,也操作车票。造成安全问题
③ 如何解决:
- 当一个线程在操作车票的时候,其他线程不能参与进来,直到线程a操作完成。
1. 同步机制
- 在java中通过同步机制处理线程安全问题
- 好处:解决了线程的安全问题
- 局限性:操作同步代码,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程。效率低
方式一:同步代码块
-
语法:
synchronized(同步监视器){//需要同步的代码...}
-
需要同步的代码:操作共享数据的代码
-
同步监视器,俗称“锁”。任何一个类的对象,都可以充当锁
- 要求:多个线程必须要共用同一把锁
- 实现Runnable接口的类的锁可以是
this
- 继承Thread类的类的锁可以是
Window.class
,Window.class只会加载一次。
-
实现:
-
class Windows implements Runnable{ private int ticket = 100; Object obj = new Object(); //同步监视器 @Override public void run() { while (true){ synchronized(obj) { //synchronized(Windows.class) //synchronized(this) if (ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket); ticket--; } else { break; } } } } }
-
方式二:同步方法
- 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的
- 同步方法仍然需要同步监视器,只是不需要我们显示声明
- 非静态方法的同步监视器:this
private synchronized void show(){...}
- 静态方法的同步监视器:当前类本身
private static synchronized void show(){...}
方式三:Lock锁
- JDK5.0以后新增
- 借助ReentrantLock类
- 方法:
lock()
、unlock()
,两个方法之间的代码为同步代码 - 此方法需要手动启动同步、手动结束同步,由上述两个方法实现
public class Test08Lock {
public static void main(String[] args) {
Windows8 w = new Windows8();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.start();
t2.start();
t3.start();
}
}
class Windows8 implements Runnable {
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@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.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
2. 死锁问题
- 不同线程分别占用对方需要同步资源不放弃,都在等待对方放弃自己需要的同步资源,形成线程死锁
- 出现死锁后,不会异常、不会提示,只是所有线程都出与阻塞状态,无法继续
解决办法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
3. 线程通信问题
① 方法:
wait()
:一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器notify()
:一旦执行此方法,就会唤醒wait的一个线程。如果有多个线程被wait,就会唤醒优先级高的线程notifyAll()
:一旦执行此方法,就会唤醒所有被wait的线程。
② 说明:
wait()、notify()、notifyAll()
三个方法必须在同步代码块或同步方法中wait()、notify()、notifyAll()
三个方法的调用者必须是同步代码块或同步方法中的同步监视器(同一把锁),否则就会出现异常wait()、notify()、notifyAll()
三个方法定义在Object类中的
交替打印案例:
public class Test09WaitAndNotifiy {
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;
@Override
public void run() {
while (true) {
synchronized (this) {
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();
}
}else {
break;
}
}
}
}
}
生产者消费者案例:
/**
* 线程通信的应用:经典例题:生产者/消费者问题:
*
* 生产者(Productor)将产品交给店员(CLerk),而消费者(Customer)从店员处取走产品,
* 店员一次只能持有固定数量的产品(比如:20),
* 如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产,
* 如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
*
* 分析:
* 1.多线程问题
* 2.共享数据:店员(或产品)
* 3.线程安全:同步机制(3中方法)
* 4.涉及线程通信
*/
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer2 consumer = new Consumer2(clerk);
producer.setName("生产者1");
consumer.setName("消费者1");
producer.start();
consumer.start();
}
}
class Clerk{
private int productCount = 0;
//生产产品
public synchronized void produceProcuct() {
if (productCount < 20) {
productCount++;
System.out.println(Thread.currentThread().getName() + ":正在生产第" + productCount + "个产品");
notify();
}else {
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProcuct() {
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(getName() + ":开始生产...");
while (true) {
try {
Thread.sleep(90);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProcuct();
}
}
}
//消费者
class Consumer2 extends Thread{
private Clerk clerk;
public Consumer2(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费...");
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProcuct();
}
}
}
六、创建线程的方式
1. 继承Thread类
2. 实现Runnable接口
3. 新增一:实现Callable接口
-
1)说明:与Runnable接口相比,Callable功能更强大
- ① 相比run()方法,可以有返回值
- ② 方法可以抛出异常
- ③ 支持泛型的返回值
- ④ 需要借助FutureTask类,比如获取返回结果
-
2)实现
-
public class Test10Callable { public static void main(String[] args) { //3.创建Callable接口实现类的对象 NumThread numThread = new NumThread(); //4.将Callable实现类的对象作为参数传递到FutureTask的构造器中 FutureTask futureTask = new FutureTask<>(numThread); //5.将FutureTask作为参数传递到Thread构造器中 new Thread(futureTask).start(); try { //6.获取Callable中call()方法的返回值 //get方法的返回值即为FutureTask构造器参数Callable实现类重写的call()方法的返回值 Object o = futureTask.get(); System.out.println("总和为:"+o); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } //1.创建实现Callable接口的实现类 class NumThread 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; } }
-
-
3)如何理解Callable接口比Runnable接口强大?
- ① call()方法可以有返回值
- ② call()方法可以抛出异常
- ③ Callable支持泛型
4. 新增二:使用线程池
-
1)好处
-
提高响应速度(减少了创建新线程的时间)
-
降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
-
便于线程管理
-
corePoolSize:核心池大小
-
maximumPoolSize:最大线程数
-
keepAliveTime:线程没有任务时,最多保持多长时间后中止
-
ExecutorService service = Executors.newFixedThreadPool(10); ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; service1.setCorePoolSize(15);
-
-
-
2)实现
-
public class Test10ThreadPool { public static void main(String[] args) { //1.提供指定数量的线程池 ExecutorService service = Executors.newFixedThreadPool(10); //设置属性 // ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; // service1.setCorePoolSize(15); //2.执行指定的线程的操作,需要提供实现接口(Runnable、Callable)的类的对象 service.execute(new NumberThread()); //适合Runnable // service.submit(); //适合Callable //3.关闭连接池 service.shutdown(); } } class NumberThread implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + ":" + i); } } } }
-
末、面试题
1. synchronized和lock异同
- 同:二者都可以解决线程安全问题
- 异:
- synchronized机制在执行完相应的同步代码后,自动释放同步监视器
- lock需要手动启动同步(lock()),也需要手动的结束同步(unlock())
2. wait()和sleep()异同
- 同:一旦执行方法都可以使当前线程进入阻塞状态
- 异:
- ① 两个方法定义的位置不同
- Thread类中声明sleep()
- Object类中声明wait()
- ② 调用要求不同
- sleep()在任何场合可以调用
- wait()必须在同步代码块或同步方法中调用
- ③ 在同步代码块或同步方法中是否释放同步监视器
- sleep():不释放
- wait():释放
- ① 两个方法定义的位置不同