4. 多线程

4.1 基本概念:程序、进程和线程

程序、进程和线程

程序:为了完成特定的任务,用某种语言编写的一组指令的集合。程序是一段静态的代码,静态对象。

进程:是程序的一次执行过程或正在运行的程序。(进程是一个任务)。进程是一个动态的过程:有产生、存在和消亡的过程——即拥有生命周期。

  • 程序是静态的,进程是动态的
  • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存空间

线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。一个进程可以包含一个或多个线程。

  • 若一个进程同一时间执行多个线程,就是支持多线程。
  • 线程作为执行和调度的单位,每个线程拥有独立的运行栈和程序计数器(pc, program counter register),线程切换开销比较小。
  • 一个进程中的多个线程可以共享相同的内存单元/内存地址空间(它们从同一堆中分配对象,方法区、堆),可以访问相同的变量和对象。使得线程间通信更高效、便捷。但是多个线程操作共享的系统资源可能会带来安全隐患。

并行与并发

  • 并行:多个CPU同时执行多个任务。
  • 并发:一个CPU(采用时间片)同时执行多个任务。如:秒杀。

多线程的优点

  • 提高程序的响应
  • 提高CPU的利用率
  • 改善程序结构

何时需要多线程

  • 程序需要同时执行两个或多个任务
  • 程序需要实现等待的任务
  • 需要后台运行的程序

多进程和多线程比较

和多线程相比,多进程缺点:

  • 创建进程比创建线程开销大
  • 进程间通信比线程间通信慢,因为线程间通信是读写同一个变量,速度很快

多进程优点:

  • 多进程稳定性比多线程高,以为多进程下,一个进程崩溃不会影响其他进程。而在多线程下,任何一个线程的崩溃会直接导致整个进程崩溃

Java语言内置了多线程支持:一个Java应用程序实际上是一个JVM进程,JVM进程用一个主线程执行main()方法,在main()内部,又可以启动多个线程。(一个java.exe,至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。)

线程的分类

Java线程分为两类:一种是守护线程,一种是用户线程。

  • 在各当面几乎是一样的,唯一区别是判断JVM何时离开。
  • 当用户线程执行结束,守护线程也会结束。
  • 守护线程用来服务用户线程的,在start() 前调用 thread.setDaemon(true) 可以把一个用户线程变为守护线程
  • Java垃圾回收机制就是一个典型的守护线程
  • 若JVM中都是守护线程,当前JVM将退出

4.2 线程的创建和使用

创建多线程有四种方法,这里有两种,后续java 1.5增加了两种新方法。

Java语言的JVM允许程序运行多个线程,通过 java.lang.Thread类来体现。

创建一个新线程有两种方法:

第一种:继承Thread 的方式

将一个类声明为 Thread 的子类,子类应重写Thread类的run方法,然后分配并启动子类的实例。

创建线程步骤

  1. 创建一个继承于 Thread 类的子类
  2. 重写 Thread 类的 run() 方法
  3. 创建 Thread 类的子类的对象
  4. 通过对象调用 start() 方法

示例:

// 1. 继承于Thread
class MyThread extends Thread{
// 2. 重新run方法
@Override
public void run() {
System.out.println("child thread name: "+getName()); // 也可以用 Thread.currentThread().getName()
// 业务写在这个方法中
}
} public class TheadTest { public static void main(String[] args) {
// 3. 创建对象
MyThread myThread = new MyThread();
myThread.setName("子线程"); // 设置线程名称
// 4. 通过对象调用 start 方法
myThread.start(); Thread.currentThread.setName("主线程"); // 设置主线程名称
System.out.println("main thread name: "+Thread.currentThread().getName()); }
} 输出结果:
main thread name: 主线程
child thread name: 子线程

如果要创建多个线程

// 如果创建多个子线程的话,需要
MyThread myThread = new MyThread();
myThread.start();
MyThread myThread2 = new MyThread();
myThread2.start();
MyThread myThread3 = new MyThread();
myThread3.start();

多个线程共享一个静态变量:

当多个线程都想要共用一个值时,比如卖票时的票数,可以将类变量设置为 staitc。

start() 方法的作用

  • 启动当前线程
  • Java虚拟机调用此线程的run方法

创建Thread 的匿名子类

简单一点的话,可以写Thread 的匿名子类

// 创建Thread 的匿名子类
new Thread(){
@Override
public void run() {
System.out.println("test2");
}
}.start();

使用Java8引入的 lambda

new Thread(()->{
System.out.println("t3");
}).start();

线程的常用方法

  • start() 启动当前线程;调用run方法
  • run() 通常需要重写Thread类的此方法,将业务写在此方法中
  • currentThread() 静态方法,返回执行当前代码的线程
  • getName() 获取当前线程的名称
  • setName() 设置当前线程的名称
  • yield() 线程让步,释放当前CPU的执行权。(暂停当前正在执行的线程,给优先级相同或更高的线程让步)
  • join() 当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被调用线程执行完为止。(在线程a中调用线程b的join(), 此时线程a进入阻塞状态,直到线程b执行完之后,线程a才结束阻塞状态,继续执行后续程序。)
  • stop() 强制线程生命期结束,不推荐
  • sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)
  • isAlive() 判断当前线程是否存活
  • getPriority() 返回此线程的优先级
  • setPriority(int newPriority) 更改线程的优先级

线程的调度

调度策略:

  • 时间片
  • 抢占式:高优先级的线程抢占CPU

Java的调度方法:

  • 同优先级线程组成先进先出队列,使用时间片策略
  • 对高优先级,使用优先调度的抢占式策略

线程的优先级

  • MAX_PRIORITY: 10 (最高优先级)
  • MIN_PRIORITY: 1 (最低优先级)
  • NORM_PRIORITY: 5 (普通优先级)

线程创建时继承父线程的优先级。

低优先及只是获得调用的概率低,并不意味着只有当高优先级的线程执行完以后才会执行低优先级。

第二种:实现 Runnable 接口的方式

第二种创建线程的方式是:创建一个实现 Runnable 接口的类,类实现了run() 方法,将类的对象传递给Thread() 并启动。

创建线程步骤

  1. 创建一个实现了 Runnable 接口的类
  2. 实现类去实现 Runnable 的抽象方法 run()
  3. 创建实现类的对象
  4. 将对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
  5. 通过 Thread 类的对象调用 start() 方法

示例:


/**
* 多线程的创建,方式二,实现Runnable接口的方式
* @author chadJ
* @create 2022-03-04 18:49
*/ //1. 创建一个实现了 Runnable 接口的类
class MyThread2 implements Runnable{ //2. 实现类去实现 Runnable 的抽象方法 run()
@Override
public void run() {
System.out.println("t2");
}
} public class ThreadRunnable { public static void main(String[] args) {
//3. 创建实现类的对象
MyThread2 t2 = new MyThread2();
//4. 将对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
Thread thread = new Thread(t2);
//5. 通过 Thread 类的对象调用 start() 方法
thread.start(); }
}

如果要创建多个线程

// 如果创建多个子线程的话
// t2 对象可以直接使用,也能共享里面的变量,因为只有一个对象
MyThread2 t2 = new MyThread2(); Thread thread = new Thread(t2);
thread.start();
Thread thread2 = new Thread(t2);
thread2.start();
Thread thread3 = new Thread(t2);
thread3.start();

start()方法如何执行到run

这里,最后的 start() 方法,是怎么执行到 MyThread2 中的 run() 方法的呢。原来在

Thread.run中有如下代码,如果target 不为空,则执行 target 的run() 方法

  public void run() {
if (target != null) {
target.run();
}
}

其中,target 是我们调用 Thread(t2) 时通过构造函数传过去的 Runnable

public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}

两种方式的比对

一种是继承Thread, 一种是实现Runnable 接口。

开发中,优先选择:实现 Runnable 接口的方式,

原因:

  • 实现的方式没有类的单继承性的局限性,可以实现了Runnable接口后,实现其他接口
  • 实现的方式更适合来处理多个线程有共享数据的情况(实现自然而然能共享数据,继承得给变量加 static)

联系:Thread 也实现的 Runnable。

相同点:两种方式都需要重写 run() 方法,将线程要执行的逻辑声明在 run() 中。

第三种:实现 Callable 接口

JDK5.0 新增两种线程创建方式:实现 Callable 接口、使用线程池 方式。

与使用 Runnable 相比,Callable 功能更强大:

  • 相比 run() 方法,可以有返回值

  • 方法可以抛出异常(之前的run()不可以抛出,因为原方法没抛出异常,重写的不能抛出)

  • 支持泛型的返回值

  • 需要借助 FutureTask 类,比如获取返回结果

    Futrue 接口

    • 可以对具体Runnable Callable 任务的执行结果进行取消、查询是否完成、获取结果等操作。
    • FutureTask 是 Future 接口的唯一实现类
    • FutureTask 同时实现了 Runnable 和 Future接口。即可以作为Runnable 被线程执行,又可以作为 Future 得到Callable 的返回值。

创建线程步骤

  1. 创建一个实现 Callable 的实现类
  2. 实现 call() 方法,将业务放在call()中。可以有返回值,可以抛出异常
  3. 创建 Callable 接口实现类的对象
  4. 将此 Callable 实现类的对象作为参数传递 FutureTask 构造器中,创建 FutureTask 对象
  5. 将FutureTask 对象作为参数传递到 Thread类构造器中(Runnable多态,因为FutureTask实现了Runnable接口),创建 Thread 对象,并调用 start()
  6. 可以使用 get() 获取call() 中的返回值。( get() 返回值即为 FutureTask 构造器参数 Callable 实现类重写 call()方法的返回值。)
package com.acfuu.java;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask; /**
* 创建线程方式三:实现Callable接口
* @author chadJ
* @create 2022-03-06 14:11
*/ // 1.创建一个实现 Callable 的实现类
class NumThread implements Callable{ // 2.实现 call() 方法,将业务放在call()中。可以有返回值,可以抛出异常
@Override
public Object call() throws Exception {
int sum=0;
for (int i = 0; i <= 100; i++) {
sum+=i;
}
return sum;
}
} public class CallableThread {
public static void main(String[] args) {
// 3. 创建 Callable 接口实现类的对象
NumThread numThread = new NumThread();
// 4. 将此 Callable 实现类的对象作为参数传递 FutureTask 构造器中,创建 FutureTask 对象
FutureTask futureTask = new FutureTask(numThread);
// 5. 将FutureTask 对象作为参数传递到 Thread类构造器中,创建 Thread 对象,并调用 start()
new Thread(futureTask).start(); try {
// 6. 可以使用 get() 获取call() 中的返回值。
// get() 返回值即为 FutureTask 构造器参数 Callable 实现类重写 call()方法的返回值。
Object r = futureTask.get();
System.out.println("返回值:"+r);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

第四种:使用线程池(常用)

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建和销毁,实现重复利用。

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用池中线程,不用每次都创建)
  • 便于线程管理
    • corePoolSize: 核心池大小
    • maximumPoolSize: 最大线程数
    • keepAliveTime: 线程没有任务时的存活时间

线程池相关 API

JDK5.0起提供了线程池API:ExecutorService 和 Executors

ExexutorService:真正的线程池接口。常见子类 ThreadPoolExecutor

  • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行 Runnable
  • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行 Callable
  • void shutdown():关闭连接池

Executors: 工具类,线程池的工厂类,用于创建并返回不同类型的线程池

  • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
  • Executors.newFixedThreadPool():创建一个可重用固定线程池数的线程池(常用)
  • Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
  • Executors.newScheduledThreadPool(n):在给定延迟后创建一个线程池

线程创建步骤

  1. 提供指定线程数量的线程池
  2. 执行指定的线程操作,需要提供实现 Runnable 接口或 Callable 接口实现类的对象
  3. 关闭连接池

示例:

package com.acfuu.java;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor; /**
* 创建线程方式四:使用线程池
* @author chadJ
* @create 2022-03-06 14:47
*/ class NumThread2 implements Runnable{ private boolean odd = true; public NumThread2(boolean odd) {
this.odd = odd;
} @Override
public void run() {
for (int i = 0; i <= 10; i++) {
if(odd){
if(i%2==1) System.out.println(Thread.currentThread().getName()+":"+i);
} else{
if(i%2==0) System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
} public class ThreadPool {
public static void main(String[] args) {
// 1. 提供指定线程数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10); // 设置线程池属性
// System.out.println(executorService.getClass());
// ThreadPoolExecutor executor = (ThreadPoolExecutor) executorService;
// executor.setCorePoolSize(1); // 设置为1后,线程池中就不是10个线程,就只有一个线程了 // 2. 执行指定的线程操作,需要提供实现 Runnable 接口或 Callable 接口实现类的对象
executorService.execute(new NumThread2(true)); // 适用于 Runnable
executorService.execute(new NumThread2(false));
//executorService.submit(xxx); // 适用于 Callable // 3. 关闭连接池
executorService.shutdown();
}
} 输出:
pool-1-thread-1:1
pool-1-thread-2:0
pool-1-thread-2:2
pool-1-thread-2:4
pool-1-thread-2:6
pool-1-thread-2:8
pool-1-thread-2:10
pool-1-thread-1:3
pool-1-thread-1:5
pool-1-thread-1:7
pool-1-thread-1:9

4.3 线程的生命周期

JDK 中的Thread.State 类定义了线程的状态。一个线程对象只能调用一次 start() 方法启动新线程(所以不能一个Thread对象多次start),并在新线程中执行 run() 方法。一旦 run() 执行完毕,线程就结束了。

Java 线程的状态有:

  • New, 新创建的线程,尚未运行
  • Runnable, 运行中的线程,正在执行 run()方法的代码
  • Blocked, 运行中的线程,因为某些操作被阻塞而挂起
  • Wating, 运行中的线程,因为某些操作在等待
  • Time Waiting, 运行中的线程,因为执行 sleep() 方法正在计时等待
  • Terminated, 线程已终止,run() 执行完毕

线程声明周期:

4.4 线程的同步

线程的同步是为了解决线程的安全问题。

4.4 线程的同步

线程的同步是为了解决线程的安全问题。

问题:比如卖票出现重票、错票,这就叫出现了线程的安全问题。

解决:当一个线程a在操作的时候,其他线程不能参与进来,直到线程a操作结束其他线程才可以开始操作。即使线程a出现了阻塞,也不能执行其他线程。

线程的安全问题

在多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待。通过加锁和解锁操作,就能保证指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期间被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区,任何时候临界区最多只有一个线程能执行。

在Java中,通过同步机制,解决线程的安全问题。

同步机制解决线程安全问题

使用synchronized (发音 /'sɪŋkrənaɪzd/),有两种方法:

  • 同步代码块
synchronized(lock) { // lock锁也叫同步监视器
// 需要被同步的代码。(操作共享数据的代码)
}
任何一个类的对象都可以充当锁。要求:多个线程必须共用同一把锁。

在实现Runnable 接口创建多线程的方式中,可以考虑使用 this 充当 lock。

在继承Thread 类创建多线程的方式中,(慎用this充当lock),考虑使用当前类充当lock。(比如 MyThread.class)
  • 同步方法

    如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

    需要同步的方法使用 synchronized 修饰:

    1.在实现 Runnable 接口的方式中:

    此种方法中,lock 是 this。

public synchronized void test(){ // 在test中,同步监视器/锁就是 this
...
} public void run(){
...
test();
...
}
`run()` 方法也可以使用 synchronized修饰, `sysnchronized void run()`,这种要确保 `run()`中使完整的同步数据。不然包裹的太多,效率会低。

2.在继承 Thread 类的方式中:

为了保证正常执行,需要多加个 static 。

此种方法中,lock 是 当前类。
public static synchronized void test2(){ // 在test2中,同步监视器/锁就是 当前类
...
} public void run(){
...
test2();
...
}
总结:

- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
- 非静态的同步方法,同步监视器是 *this*
- 静态的同步方法,同步监视器是 *当前类本身*。

线程同步优缺点

  • 同步的方式,解决了线程的安全问题。
  • 操作同步代码时,只能有一个线程参与,其他线程等待。相当于一个单线程的过程,效率低。

只有有共享数据的代码才需要使用线程的同步解决问题。

Lock 锁方式解决线程安全问题

从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁来实现同步。同步锁使用Lock对象充当。

1.使用ReentrantLock

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.start();
t2.start();
t3.start();
}
} class Window implements Runnable{ private int ticket = 100; // 1. 实例化 ReentrantLock
private final ReentrantLock lock = new ReentrantLock(); // 参数 fair=true/false, 公平或者非公平 @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();
}
}
}

4.5 线程安全的 懒汉式的单例模式

class Bank{
private Bank(){} private static Bank instance = null; // 普通获取实例的方法
public static Bank getInstance(){
if(instance==null){
instance = new Bank();
}
return instance;
} // 线程安全的获取实例的方法一
public static synchronized Bank getInstance2(){
if(instance==null){
instance = new Bank();
}
return instance;
} // 线程安全的获取实例的方法二,其中还有两种写法,后一种效率高些
public static Bank getInstance3(){
// 效率不高
/*
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; }
}

4.6 线程的死锁问题

死锁:

  • 不同的线程分别占用对方需要的同步资源,都在等待对方放弃自己需要的同步资源,形成了线程的死锁。
  • 死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。

说明:

  • 出现死锁后,不会异常,不会出现提示,所有线程都处于阻塞状态,无法继续运行
  • 在使用同步时,要避免出现死锁

解决方法:

  • 专门的算法、原则
  • 尽量较少同步资源的定义
  • 尽量避免嵌套同步

示例,下面的代码会出现死锁,程序没输出没结束。在第一个线程运行到sleep的时候,线程二继续运行,结果都需要sb1 sb2,产生了死锁。

    public static void main(String[] args) {
StringBuffer sb1 = new StringBuffer();
StringBuffer sb2 = new StringBuffer(); new Thread(()->{
synchronized (sb1){
sb1.append("a");
sb2.append("1"); try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} synchronized (sb2){
sb1.append("b");
sb2.append("2"); System.out.println(sb1);
System.out.println(sb2);
}
}
}).start(); new Thread(new Runnable() {
@Override
public void run() {
synchronized (sb2){
sb1.append("c");
sb2.append("3"); try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} synchronized (sb1){
sb1.append("d");
sb2.append("4"); System.out.println(sb1);
System.out.println(sb2);
}
}
}
}).start();
}

4.7 线程的通信

多线程协调(线程通信)问题使用 wait()notify()

示例:两个线程交替打印1-100

/**
* 线程通信的示例:两个线程交替打印1-100
* @author chadJ
* @create 2022-03-05 22:31
*/ class Number implements Runnable{
private int number = 1; @Override
public void run() {
while(true){ synchronized (this) { // 唤醒其他线程
notify(); if(number<101){
System.out.println(Thread.currentThread().getName()+":"+number);
number++; try {
// 使得调用wait()方法的线程进入阻塞状态,并且wait回释放锁
wait();
} catch (InterruptedException e) {
e.printStackTrace();
} }else{
break;
}
} }
}
} public class ThreadCommunication {
public static void main(String[] args) {
Number n = new Number(); Thread t1 = new Thread(n);
Thread t2 = new Thread(n); t1.setName("线程1");
t2.setName("线程2"); t1.start();
t2.start(); }
}

线程通信涉及到的方法:

  • wait() 一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器(锁)
  • notity() 执行此方法,会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级最高的线程
  • notifyAll() 执行此方法,会唤醒所有被 wait 的线程

说明:

  • 上述三个方法 wait() notify() notifyAll() ,必须使用在同步代码块或同步方法中。Lock想要线程通信使用其他方法
  • wait() notify() notifyAll() 的调用者必须使同步代码块或同步方法中的同步监视器,否则会出现 IllegalMonitorStateException。比如
class Test implements Runnable{
...
public void run() {
synchronized (this) {
notify();
...
wait();
}
}
或 class Test implements Runnable{
...
public void run() {
synchronized (obj) {
obj.notify();
...
obj.wait();
}
}
  • 这三个方法是定义在 java.lang.Object 类中的。(从上一条可知,任何一个对象都要能够调用 wait() notify() notifyAll() 方法)

4.8 线程通信应用:生产者消费者问题

描述:生产者将产品交给店员,消费者从店员处取走商品,店员一次能持有产品数量有容量限制,如果生产者试图生产更多的产品,店员会叫停生产者,如果有空位了再通知生产者继续生产。如果店中没有商品,店员会叫停消费者,如果有产品了再通知消费者来取走产品。

分析:

  • 是否是多线程问题
  • 是否有共享数据
  • 如何解决线程安全问题?同步机制,有三种方法
  • 是否涉及到线程通信
/**
* 线程通信应用,生产者、消费者问题。 实现Runnable方式
* @author chadJ
* @create 2022-03-06 13:13
*/ /**
* 店员类
*/
class Clerk{
// 商品数量
private int goodsNum = 0; public synchronized void createProduct() {
if(goodsNum<20){
goodsNum++;
System.out.println(Thread.currentThread().getName()+": 生产商品 "+goodsNum); notify();
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} public synchronized void useProduce() {
if(goodsNum>0){ System.out.println(Thread.currentThread().getName()+": 消费商品 "+goodsNum);
goodsNum--; notify();
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} class Productor implements Runnable{ private Clerk clerk; public Productor(Clerk clerk) {
this.clerk = clerk;
} @Override
public void run() {
System.out.println(Thread.currentThread().getName()+":开始生产商品");
while(true){
try {
Thread.sleep(30); // sleep模拟生产过程耗费的时间
} catch (InterruptedException e) {
e.printStackTrace();
} clerk.createProduct();
}
}
} class Customer implements Runnable{
private Clerk clerk; public Customer(Clerk clerk) {
this.clerk = clerk;
} @Override
public void run() {
System.out.println(Thread.currentThread().getName()+":开始消费商品"); while(true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} clerk.useProduce();
}
}
} public class ProductorCustomerTest {
public static void main(String[] args) {
Clerk clerk = new Clerk(); Thread p1 = new Thread(new Productor(clerk));
p1.setName("生产者");
p1.start(); Thread c1 = new Thread(new Customer(clerk));
c1.setName("消费者1");
c1.start(); Thread c2 = new Thread(new Customer(clerk));
c2.setName("消费者2");
c2.start(); Thread c3 = new Thread(new Customer(clerk));
c3.setName("消费者3");
c3.start();
}
}

4.9 面试题

synchronized 与 Lock 的异同?

相同:二者都可以解决线程安全的问题

不同:

  • Lock 是显式锁(需要手动启动和释放锁),synchronized 是隐式锁,出了作用域自动释放
  • Lock 只有代码块锁,synchronized 有代码块锁和方法锁
  • 使用 Lock 锁,JVM将花费较少时间来调度线程,性能更好

优先使用顺序:(建议)

Lock → 同步代码块(已经进入了方法体,分配了相应的资源)→ 同步方法(方法体之外,作用整个方法)

如何解决线程安全问题,有几种方式

同步机制说两种三种都可以,要都讲到

两种:synchronized方式(又分同步代码块和同步方法)和Lock方式

三种:synchronized 同步代码块、synchronized 同步方法 和 Lock 方式

sleep() 和 wait() 的异同?

相同点:都可以让线程进入阻塞状态

不同点:

  • 两个方法声明的位置不同:Thread 类中声明的 sleep();Object 类中声明的 wait()
  • 调用要求不同:sleep() 可以在任何需要的场景下调用;wait() 必须使用在同步代码块或同步方法中。
  • 关于是否释放同步监视器:如果两方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait() 会释放锁。

如何理解实现 Callable 接口的方式创建多线程比实现 Runnable 接口创建多线程方式更强大?

  • call() 可以有返回值。可以抛出异常,被捕获后获取异常信息
  • Callable 是支持泛型的

创建多线程有几种方式?

四种:继承Thread类,实现Runnable接口,实现 Callable 接口,使用线程池(后两种为JDK5.0 新增)。

JavaSE高级编程之多线程的更多相关文章

  1. 多线程 用户级线程和内核级线程 from C++多核高级编程

    转 http://book.51cto.com/art/201006/206946.htm 6.1.1 用户级线程和内核级线程 2010-06-21 20:37 齐宁/董泽惠 译 清华大学出版社 字号 ...

  2. Objective-C 高级编程:iOS与OS X多线程和内存管理

    <Objective-C 高级编程:iOS与OS X多线程和内存管理> 基本信息 原书名: Pro Multithreading and Memory Management for iOS ...

  3. 第十章:Python高级编程-多线程、多进程和线程池编程

    第十章:Python高级编程-多线程.多进程和线程池编程 Python3高级核心技术97讲 笔记 目录 第十章:Python高级编程-多线程.多进程和线程池编程 10.1 Python中的GIL 10 ...

  4. (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  5. (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  6. (五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  7. (八) 一起学 Unix 环境高级编程 (APUE) 之 信号

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  8. (九) 一起学 Unix 环境高级编程 (APUE) 之 线程

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  9. (十) 一起学 Unix 环境高级编程 (APUE) 之 线程控制

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

随机推荐

  1. IoC容器-Bean管理(bean作用域)

    IoC操作Bean管理(bean作用域) 1,在Spring里面,设置创建bean实例是单实例还是多实例 2,在Spring里面,默认情况下,bean是单实例对象 3,如何设置单实例还是多实例 (1) ...

  2. new实例化和反射实例化有什么区别?

    在工厂设计模式中,使用反射实例化,子类可以随便增加,工厂类不需要做任何的修改 使用反射之后最大的好处就是解耦合

  3. Kubernetes 证书默认1年过期时间修改

    使用过的kubeadm搭建K8s集群的朋友知道,默认自动生成的证书有效期只有 1 年,因此需要每年手动更新一次证书,这种形式显然对实际生产环境来说很不友好:因此下面教给大家修改这个过期时间的终极方法. ...

  4. python06day

    Now代码1005行 回顾 字典的初识 查询速度快,{'name':'tangdaren'},存储大量关联型数据 键:int.str(bool tuple不常用)不可变的数据类型 值:任意数据类型 3 ...

  5. sms短信网关对接

    因为工作的需求,需要对接短信网关,业务上就是一个注册用户时,需要发送手机验证码;可能别的公司都是使用第三方接口,但是大点的公司,为了安全,他们都有自己的短信消息中心(SMSC) 1.业务需求 - 1. ...

  6. PHP中的单引号跟双引号的区别

    不同点: 单引号只能解析转义字符"'"和"\",其他的原样输出.

  7. linux+nginx+tomcat负载均衡,实现session同步

    第一部分:nginx反向代理tomcat 一.软件及环境 软件 系统 角色 用途 安装的软件 ip地址 Centos6.5x86_64 nginx 反向代理用户请求 nginx 172.16.249. ...

  8. HEAAN新版学习

    本篇文章对最新版的HEAAN库进行研究,老版的介绍见 HEAAN库学习 主要参考:slide-HEAAN.pdf HEAAN介绍 HEAAN是一个支持在加密的复数数组之间进行操作的库,方案的安全性取决 ...

  9. 一劳永逸Java环境配置,以及编写我的第一个Java程序

    Java环境配置,以及编写我的第一个Java程序 配置步骤 1.下载jdk 2.安装步骤 3.配置环境 4.我的第一个Java程序 配置步骤 网上的教程有很多,方法也都不尽相同.今天我就分享一下我的配 ...

  10. Linux vi 命令 – 文本编辑器

    vi命令是linux系统字符界面下的最常用的文本编辑器. vi编辑器是所有linux的标准编辑器,用于编辑任何ASCⅡ文本,对于编辑源程序尤其有用.iv编辑器功能非常强大,可以对文本进行创建,查找,替 ...