尚硅谷_Java零基础教程-java入门必备-初学者基从入门到精通全套完整版(宋红康主讲)
P430-P447
相关题目
- 画图说明线程的生命周期,以及各状态切换使用到的方法等。
- 同步代码块中涉及到同步监视器和共享数据,谈谈你对同步监视器和共享数据的理解,以及注意点。
- sleep()和wait()的区别。
- 写一个线程安全的懒汉式
- 创建多线程有哪几种方式。
文章目录
2.线程的创建
比较创建线程的两种方式:继承Thread和实现Runnable
开发中优先选择Runnable:
- 实现的方式没有类的单继承的局限性
- 实现的方式更适合来处理多个线程共享数据的情况。
问题
-
谈谈对程序、进程、线程的理解
答 :- 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即一段静态的代码,静态对象。
- 进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
程序是静态的,进程是动态的。
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。 - 线程(Thread),进程可进一步细分为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的。
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使线程间通信更简便、高效。但多个线程共享的系统资源可能就会带来安全隐患。
-
代码完成继承Thread的方式创建分线程,并遍历100以内的自然数
-
代码完成继承Thread的方式创建分线程,并遍历100以内的自然数
-
对比两种创建方式
相同点:都需要重写run()方法。目前两种方式,要想启动线程,都是调用Thread.run()方法
联系: public class Thread implements Runnable -
对IDEA中Project和Module的理解
Project是顶级结构
Module可以在project下建立不同的模块
3.线程的生命周期
线程的几种状态:
- 新建new:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
- 新建–>就绪:调用start()
- 就绪Runnable:处于新建状态的线程被start()之后,将进入线程队列等待CPU时间片,此时它已经具备了运行的条件,只是没分配到CPU资源
- 就绪–>运行:获取CPU执行权
- 运行Running:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
- 运行–>就绪:失去CPU执行权或yeild()
- 运行–>死亡:stop();执行完;异常Error/Exception未处理
- 运行–>阻塞:sleep(long t);B.join();等待同步锁;wait();suspend()挂起,可能会导致死锁;
- 运行–>就绪:失去CPU执行权或yeild()
- 阻塞Blocking:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
- 阻塞–>就绪:sleep(long t)时间到;B.join()结束;获取同步锁;notify();resume()(对应suspend)
- 死亡Dead:线程完成了它的全部工作或线程被提前强制性中止或出现异常导致结束。
4.线程的同步
问题的提出
- 多个线程的执行的不确定性引起执行结果的不稳定性
- 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。
同步机制
方法一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:
- 操作共享数据的代码,即为需要被同步的代码
- 共享数据:多个线程共同操作的变量
- 同步监视器:俗称“锁”。任何一个类的对象都可以充当锁。
要求:多个线程必须要共用同一把锁。
方法二:同步方法
- 同步方法仍然涉及到同步监视器,只是不需要显式声明
- 非静态的同步方法,同步监视器是this;静态的同步方法,同步监视器是:当前类本身
*线程安全的懒汉式单例模式
public class BankTest {
private static Bank instance = null;
public static Bank getInstance() {
//方式一:效率稍差
// synchronized (Bank.class) {
// if (instance==null){
// instance=new Bank();
// }
// return instance;
// }
//方式二:效率更高
if (instance==null){
synchronized (Bank.class) {
if (instance==null){
instance=new Bank();
}
}
}
return instance;
}
}
class Bank{
}
死锁问题
死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决办法
- 专门的算法、原则
- 尽量减少同步资源 的定义
- 尽量避免嵌套同步
Lock
- 从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock接口,它用于与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
class Window implements Runnable{
private ReentrantLock lock = new ReentrantLock();
private int ticket = 100;
@Override
public void run() {
while (true){
try{
lock.lock();
if (ticket>0){
System.out.println(Thread.currentThread().getName() + "售出,票号为:" + ticket);
ticket--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}finally {
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w,"窗口1");
Thread t2 = new Thread(w,"窗口2");
Thread t3 = new Thread(w,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
synchronized与Lock的异同
同:都可以解决线程安全问题
异:
- synchronized机制在执行完相应的同步代码之后,自动的释放同步监视器
- Lock需要手动的启动同步(lock()),也需要手动的结束同步(unlock())。
5.线程的通信
问题:实现线程交替打印1-100。(类似面试题,交替打印“A”“B")
/*
交替打印1-100
*/
class Number implements Runnable{
private int number = 1;
public void run(){
while (true){
synchronized (this) {
notify();
if (number<=100){
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
涉及到的三个方法:
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级最高的 || 随机一个。
- notifyAll():一旦执行此方法,就会唤醒被wait的所有线程。
说明:
- 这些方法必须使用在同步方法或同步代码块中。
- 这三个方法的调用者必须是同步方法或同步代码块中的同步监视器。否则,会出现
java.lang.IllegalMonitorStateException
异常。 - 这三个方法是定义在java.lang.Object类中。因为理论上同步监视器可以是任意一个类的对象,都继承object
sleep()和wait()方法的异同
- 相同点:
- 一旦执行方法,都可以使得当前的线程进入阻塞状态。
- 不同点:
- (1) 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()。
- (2) 调用的要求不同:sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或同步方法中。
- (3) 关于是否释放同步监视器:sleep()不会释放同步监视器;wait()会释放。
经典例题:生产者/消费者问题
描述
- 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产。如果店中没有产品了,会通知消费者等一下;如果店中有产品了再通知消费者来取走产品。
- 这里可能出现两个问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到。
- 消费者比生产者快时,消费者会取相同的数据。
分析
- 是否是多线程问题:是。有生产者线程,消费者线程。
- 是否有共享数据:是,店员(或产品)
- 如何解决线程的安全问题:同步机制,三种方法。
- 是否涉及线程的通信:是。
实现
clerk:
package learn.concurrency.consumerAproducer2;
public class Clerk {
private int num = 10;
static final int MAX_NUM = 20;
//生成产品
public synchronized void produceProduct() {
// notifyAll();
if (num<MAX_NUM){
num++;
System.out.println(Thread.currentThread().getName() + "开始生产第" + num + "个产品.");
notify();
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProduct() {
if (num>0){
System.out.println(Thread.currentThread().getName() + "开始消费第" + num + "个产品.");
num--;
notify();
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
生产者:
package learn.concurrency.consumerAproducer2;
import java.util.concurrent.BlockingQueue;
public class Producer extends Thread{
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
public Producer(Clerk clerk, String name) {
super(name);
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + "开始生成产品……");
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
消费者:
package learn.concurrency.consumerAproducer2;
public class Consumer extends Thread{
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
public Consumer(Clerk clerk, String name) {
super(name);
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + "开始消费产品……");
while (true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
测试Main:
package learn.concurrency.consumerAproducer2;
public class MainTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p = new Producer(clerk,"生产者1");
Consumer c = new Consumer(clerk, "消费者1");
p.start();
c.start();
new Producer(clerk,"生产者2").start();
new Producer(clerk,"生产者3").start();
}
}
6. JDK5.0新增线程创建方式
新增方式一:实现Callable接口
- 与使用Runnable相比,Callable功能更加强大:
- 相比run()方法,call()方法可以有返回值。
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
- Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成,获取结果等。
- FutureTask是Future的唯一实现类
- FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Future获得Callable的返回值。
- 实现步骤:
- 1.实现一个Callable的实现类,将此线程需要执行的操作声明在call()方法中。
- 2.实现一个Callable实现类的对象
- 3.将此Callable实现类的对象,传递到FutureTask 的构造器中,创建一个FutureTask 的对象
- 4.将FutureTask 对象作为参数传递到Thread类的构造器中,创建Thread对象,并start();
- 5.获取Callable中call()方法的返回值
// 第1步
class NumTread implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i%2==0){
System.out.println(i);
sum+=(i+1);
}
}
return sum;
}
}
public class ThreadNew{
public static void main(String[] args) {
// 第2步
NumTread numTread = new NumTread();
// 第3步
FutureTask futureTask = new FutureTask(numTread);
// 第4步
new Thread(futureTask).start();
try {
// 第5步
Object x = futureTask.get();
System.out.println("总和为:"+x);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
- 如何理解实现Callable接口的方式创建多线程比实现Runnable的方式更强大?
- call()方法有返回值
- call()方法可以抛出异常,被外面的操作捕获,获取异常信息。
- Callable支持泛型。
新增方式二:使用线程池
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响较大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理:
corePoolSize
:核心池的大小maximumPoolSize
:最大线程数keepAliveTime
:线程没有任务时最多保持多长时间后会结束
线程池相关API
- JDK5.0开始提供了线程池相关API:ExecutorService和Executors
- ExecutorService:真正的线程池接口。常见子类:ThreadPoolExecutor
public void execute(Runnable command) {}
:执行任务/命令,没有返回值,一般用来执行Runnable<T> Future<T> submit(Callable<T> task);
:执行任务,有返回值,一般用来执行 Callablevoid shutdown();
关闭连接池
- Executors:工具类、线程池的工厂类,用来创建并返回不同类型的线程池
public static ExecutorService newCachedThreadPool() {
创建一个可根据需要创建新线程的线程池public static ExecutorService newFixedThreadPool(int nThreads) {
创建一个可重用固定线程数的线程池public static ExecutorService newSingleThreadExecutor() {
创建一个只有一个线程的线程池public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行。