Java-多线程
一、概述
1、进程与线程
- 进程:进程是程序运行所使用的基本单位。在windows系统上,一个运行的exe就是一个进程。
- 线程:线程是比进程更小的cpu调度和分配资源的基本单位。由于一个进程的量级过大,无法调度,所以将它划分为更小的单位——线程由cpu去调度与分配。
- 通俗而言:一个进程可以执行同时N件事,而执行N件事的是线程。
- 两者资源边界的不同:进程的内存资源是相互隔离的,但是多个线程之间是共享的。X宝与X信两个进程之间的资源是隔离的,但是它们各自内部的线程是各自共享内存资源的。
- 两者关系:一个进程至少包括一个或多个线程,而一个线程只属于一个进程。
2、并发和并行
? 场景1:班级大扫除,下午4点的开始,小红在扫地,小明在擦窗,小李在擦黑板。下午4点15分结束
? 场景2:班级大扫除,下午4点开始,小红扫了15分钟地,然后擦了15分钟窗,最后擦了5分中黑板。下午4点35分结束。
? 总结:无论是场景1还是场景2,结果都是完成了班级大扫除,但是场景1的三件工作是同时执行的,这就是并行。而场景2是一件事结束然后快速切换到下一件事去执行,这便是并发。
- 并行:并行指两个或两个以上的事件同一时刻发生
-
并发:并发指两个或两个以上的事件同一时间间隔发生
-
操作系统平时的多个进程同时运行并不是真的同时运行,这是一个假象,它实际上是一种并发,多进程运行时操作系统通过快速切换上下文实现的,只不过计算机切换过快,我们用户无感而已。
3、线程的状态
- 新建状态(New)
万事万物都不是凭空出现的,线程也一样,它被创建后的得状态被称为 新建 状态
比如:
Thread thread = new Thread();
- 可运行状态(Runable)
线程被创建后是不能使用的,就是让用户在此期间设置一些属性
比如:
// 设置类加载器
thread.setContextClassLoader(System.class.getClassLoader());
// 设置线程名称
thread.setName("商品服务-product-service");
// 是否为守护线程/用户线程
thread.setDaemon(false);
// 设置线程优先级
thread.setPriority(5);
? 通过 thread.start() 方法开启线程,开启后意味着该线程 “能够” 运行,并不意味着一定会运行,因为它要抢占资源,获取CPU的使用权后,才能运行。所以此状态称为 可运行状态。
- 运行状态(Running)
线程通过努力,获得了CPU的使用权,就会进入执行程序,此时状态被称为 运行状态。
- 阻塞状态(BLOCKED)
多线程抢占CPU资源,同一时刻仅有一个线程进入临界区,为保证对资源访问的线程安全,同一时刻仅有一个线程进入 synchronized 同步块,而其他未获得访问权的线程将进入 阻塞状态 。
- 等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。
- 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
- 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 睡眠状态 TIMED_WAITING(sleeping)
通过调用对象的wait(time)方法或调用线程的sleep(time)/join(time),等待/睡眠指定的时间,此时该线程会进入TIMED_WAITING(sleeping) 状态,直接时间已到,会进入Runnable状态,重新抢占CPU资源。
- 等待状态 WAITING
通过调用对象的wait()方法,让抢占资源的线程等待某工作的完成,或主动join()其他线程,让当前线程释放资源等待被join的线程完成工作,而该线程将进入 等待状态 。
- 死亡状态(Dead)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
小结:
线程状态有 5 种,新建,就绪,运行,阻塞,死亡
下面几个行为,会引起线程阻塞。
- 主动调用 sleep 方法。时间到了会进入就绪状态
- 主动调用 suspend 方法。主动调用 resume 方法,会进入就绪状态
- 调用了阻塞式 IO 方法。调用完成后,会进入就绪状态。
- 试图获取锁。成功的获取锁之后,会进入就绪状态。
- 线程在等待某个通知。其它线程发出通知后,会进入就绪状态
4、单线程与多线程
- 单线程:如果有多个任务,当前任务结束后,线程才会执行下一个任务。
- 多线程:如果有多个任务,可以同时执行,这里的同时执行指并发执行
5、程序执行原理(调度方式)
? 在操作系统中,有很多种调度方式,这里介绍分时调度和抢占式调度,JAVA中使用的是抢占式调度,所以主要介绍抢占式调度
-
分时调度:所有线程轮流使用cpu使用权,平均分配每个CPU的时间
-
抢占式调度:每个线程都有其优先级,优先让优先级高的进程使用cpu,如果优先级相同,则随机选择执行
? (1)CPU其使用抢占式调度模式在多个线程间进行着高速切换
? (2)对于CPU一个核而言,某个时刻只能进行执行一个线程,而CPU在多个线程间切换速度相对我们比较快,看起来好像“同时”执行
? (3)多线程程序并不能提高程序运行速度,但能提高程序运行效率,让cpu使用率更高。
6、主线程
? Java程序在执行过程中,先启动JVM,并加载对应的class文件,JVM会从main方法开始执行我们的程序代码,一直执行到main方法结束。这个步骤是有一个线程来执行的,这个线程就是主线程。当程序的主线程执行时,如果遇到了循环而导致程序在制定位置停留时间过程,则无法马上执行下面的程序,需要等待循环结束才能往后直行。那么能否实现一个主线程执行循环功能,另一个线程执行其他代码,最终实现多部分代码同时执行的效果呢?多线程便是解决这个问题的。
7、多线程内存理解
- 多线程在执行的时候,是在栈内存中的,每一个执行线程都有一片自己的所属栈内存空间,进行方法的压栈和出栈
- 当执行线程的任务结束了,线程自动在栈内存中释放,当所有的执行线程都结束了,进程也就结束了
二、线程的创建
1、继承Thread创建线程
-
定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务,因此把run()方法称为执行体。
-
创建Thread子类的实力,创建了线程对象。
-
调用新城对象的start()方法来启动该线程
public class FirstThreadTest extends Thread {
int i = 0;
//重写run方法,run方法的方法体就是现场执行体
public void run() {
for (; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
//currentThread() 返回当前线程对象
//getName 返回当前线程名字
System.out.println(Thread.currentThread().getName() + " : " + i);
if (i == 50) {
//创建线程对象并执行start()方法
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}
分析:
- 在主线程中创建自定义线程对象,调用star方法开启新线程,让新的线程执行程序,jvm再调用线程中的run方法
- 创建新的线程后,会产生两个执行路径,都会被CPU执行,CPU有自己的选择执行权力,所以会出现随机的执行结果
- 可以理解为两个线程在抢夺CPU的资源(时间)
注:线程对象调用 run 方法和调用 star 方法的区别:
(1) 线程对象调用 run 方法不开启线程,仅仅是对象调用方法
(2) 线程对象调用 star 方法开启线程,并让 JVM 调用 run 方法在开启的线程中执行
思考:为什么不直接创建Thread对象并start?
Thread t1 = new Thread;
t1.start();
答:以上代码没有语法错误,并不会报错,但是该线程进入运行状态时执行的run方法是Thread类当中的,Thread中的run方法并不是我们实际业务中需要的。Thread 类已经定义了线程任务的编写位置(run 方法),我们只需要继承Thread类并重写一个run方法足够了。
2、实现Runnable接口创建线程
Runnable 接口用来指定每一个线程要执行的任务,包含了一个 run 的无参数抽象方法,需要由接口实现重写该方法。此创建线程的方法是声明实现 Runnable 接口的类,该类实现 run 方法,然后创建 Runnable 的子类对象,传入到某个线程的构造方法中,开启线程
(1) 创建步骤:
- 定义类实现 Runnable 接口
- 重写接口中的 run 方法
- 创建 Thread 类的方法
- 将 Runnable 接口的子类对象作为参数传递给 Thread 类的构造函数
- 调用 Thread 类的 star 方法开启线程
定义实现类接口
//定义实现类接口
public class myRunnable implements Runnable {
//重写run方法
public void run()
{
for(int i = 0;i < 5;i++)
{
System.out.println("myRunnable线程正在执行!");
}
}
public static void main(String[] args)
{
//创建线程执行目标类对象
myRunnable mR = new myRunnable();
//将Runnable接口的子类对象作为参数传递给Thread类的构造函数
Thread t1 = new Thread(mR);
Thread t2 = new Thread(mR);
//开启线程
t1.start();
t2.start();
for(int i = 0;i < 5;i++)
{
System.out.println("main线程正在执行!");
}
}
}
3、线程的匿名内部类
使用线程的匿名内部类方式,可以方便的实现每个线程执行不同线程任务操作
方法一:重写 Thread 类中的方法创建线程
new Thread() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()
+ "...X...." + x);
}
}
}.start();
方法二:使用匿名内部类的方式实现 Runnable 接口,重写 Runnable 接口中的 run 方法
Runnable r = new Runnable() {
public void run() {
for (int x = 0; x < 40; x++) {
System.out.println(Thread.currentThread().getName()
+ "...Y...." + x);
}
}
};
new Thread(r).start();
JDK5.0以后新增的两种方式
4、通过Callable和Future创建线程
- 1.创建一个实现Callable的实现类
- 2.实现call方法,将此线程需要执行的操作声明在call()中
- 3.创建Callable接口实现类的对象
- 4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
- 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
- 6.获取Callable中call方法的返回值
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在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;
}
}
public class ThreadNew {
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.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
- call()可以返回值的。
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable是支持泛型的
5、使用线程池
1.提供指定线程数量的线程池
2.执行指定的线程的操作(需要提供实现Runnable接口或Callable接口实现类的对象)
3.关闭连接池
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);
}
}
}
}
class NumberThread1 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 service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//3.关闭连接池
service.shutdown();
}
}
说明:
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没任务时最多保持多长时间后会终止
6、创建线程的三种方式对比
使用Runnable和Callable方式:
优势:
- 避免了单继承的局限性,所以此方法较为常用
- 将线程分为两部分,一部分线程对象,一部分线程任务,更加符合面向对象思想(继承 Thread 类线程对象和线程任务耦合在一起)
- 将线程任务单独分离出来封装成对象,类型就是 Runnable 接口类型
- Runnable 接口对线程对象和线程任务进行解耦,降低紧密性或依赖性,创建线程和执行任务不绑定
劣势:
1、编程变复杂了
使用Thread类创建线程方式:
优势:编程简单,易懂
劣势:只能单继承
三、常见的线程方法
- start():启动当前线程;调用当前线程的run()
- run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
- currentThread():静态方法,返回执行当前代码的线程
- getName():获取当前线程的名字
- setName():设置当前线程的名字
- *yield():释放当前cpu的执行权
- join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
- stop():已过时。当执行此方法时,强制结束当前线程。
- sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
- isAlive():判断当前线程是否存活
线程的优先级:
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5 -->默认优先级
2.如何获取和设置当前线程的优先级:
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。
线程通信:wait() / notify() / notifyAll() :此三个方法定义在Object类中的。
四、守护线程
? Java程序入口就是由JVM启动main
线程,main
线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。
? 如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。
? 但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:
class TimerThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println(LocalTime.now());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
}
如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?
? 然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?
? 答案是使用守护线程(Daemon Thread)。
? 守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
? 因此,JVM退出时,不必关心守护线程是否已结束。
? 如何创建守护线程呢?方法和普通线程一样,只是在调用start()
方法前,调用setDaemon(true)
把该线程标记为守护线程:
Thread t = new MyThread();
t.setDaemon(true);
t.start();
在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
五:线程安全问题
1、出现的原因
? 当某个线程执行的过程中,尚未操作完成时,其他线程参与进来,产生了错误的数据。若让当前线程“睡眠(sleep(long millitime))”的时间越长,出现这类情况的概率往往越大。这就是线程安全问题。
例如在同一个电影院,有三个售票窗口同时卖同一场电影的票(每张电影票上会打印唯一的流水号,以显示卖的是第几张票),若三个售票窗口同时卖票,就有可能会出现同一个流水号的情况。
2、解决方法
在Java中,我们通过同步机制,来解决线程的安全问题。
2.1 方式一:同步代码块
synchronized()同步监视器{
}
说明:
- 操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
- 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
- 补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
2.2 方式二:同步方法
在实现多线程的方法中加上synchronized锁,如
private synchronized void show(){}
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
关于同步方法的总结:
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身
2.3 方式三:Lock锁 — JDK5.0新增
1.实例化ReentrantLock
2.调用锁定方法lock()
3.调用解锁方法:unlock()
代码如下(示例):
class Window 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.4 synchronized 与 Lock的异同?
- 相同:二者都可以解决线程安全问题
- 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。 Lock需要手动的启动同步(lock(),同时结束同步也需要手动的实现(unlock())
使用的优先顺序:
Lock —> 同步代码块(已经进入了方法体,分配了相应资源 ) —> 同步方法(在方法体之外)
使用同步方式的利弊:
利:同步的方式,解决了线程的安全问题。
弊:操作同步代码时,只能一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
3.死锁问题
3.1、可重入锁
? JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
public class Counter {
private int count = 0;
public synchronized void add(int n) {
if (n < 0) {
dec(-n);
} else {
count += n;
}
}
public synchronized void dec(int n) {
count += n;
}
}
? 观察synchronized
修饰的add()
方法,当知道到add()
方法内部,当n<0的情况将去调用dec()
方法,将会再去获取到this
锁。
3.2 死锁
概念:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
说明:出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续。
我们使用同步时,要避免出现死锁。
例如:
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}
public void dec(int m) {
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
} // 释放lockA的锁
} // 释放lockB的锁
}
在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行add()
和dec()
方法时:
- 线程1:进入
add()
,获得lockA
; - 线程2:进入
dec()
,获得lockB
。
随后:
- 线程1:准备获得
lockB
,失败,等待中; - 线程2:准备获得
lockA
,失败,等待中。
此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。
那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA
,再获取lockB
的顺序,改写dec()
方法如下:
public void dec(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
} // 释放lockB的锁
} // 释放lockA的锁
}
六、等待和唤醒机制
? 等待唤醒机制是为了方便处理进程之间通信的手段,多个线程在处理同一个资源时,由于处理的动作(线程的任务)不行同,为了使各个线程能够有效的利用资源,便采取了等待唤醒机制。等待唤醒机制涉及到的方法:
- wait():等待。将正在执行的线程释放其执行资格和执行权,并存储到线程池中
- notify():唤醒。唤醒线程池中被 wait() 的线程,一次唤醒一个,而且是任意的
- notifyAll():唤醒全部。可以将线程池中的所有 wati() 线程都唤醒
注:
- 这些方法都是在同步中才有效,在使用时必须注明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程
- 因为这些方法在使用的时候要注明所属的锁,而锁又是任意对象,所以这些方法是定义在 Object 类中的
代码实例:
来看一个例子,现有Person类,存储了姓名和年龄,使用 inPut 线程对 Person 类输入信息,使用 outPut 线程对 Person 类获取打印信息
//模拟Person类
public class Person {
String name;
int age;
boolean flag = false;
//输入线程任务inPut类
public class inPut implements Runnable {
private Person p;
int count = 0;
public inPut(Person p) {
this.p = p;
}
public void run() {
while (true)
{
synchronized (p)
{
if(p.flag)
{
try {
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(count % 2 == 0)
{
p.name = "儿童";
p.age = 3;
}
else
{
p.name = "老人";
p.age = 99;
}
p.notify();
p.flag = true;
}
count++;
}
}
}
//输出线程任务outPut类
public class outPut implements Runnable {
private Person p;
public outPut(Person p)
{
this.p = p;
}
public void run() {
while (true)
{
synchronized (p)
{
if(!p.flag)
{
try {
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(p.name + ":" + p.age + "岁");
p.notify();
p.flag = true;
}
}
}
}
//在主线程中调用
public static void main(String[] args)
{
Person P = new Person();
inPut in = new inPut(P);
outPut out = new outPut(P);
Thread T1 = new Thread(in);
Thread T2 = new Thread(out);
T1.start();
T2.start();
}
分析:
- 输入 inPut 类:输入完成后,必须等待输出结果打印结束才能进行下一次赋值,赋值后,执行wait()方法永远等待,直到被唤醒,唤醒后重新对变量赋值,赋值后再唤醒输出线程 notify(),自己再wait()
- 输出 outPut 类:输出完成后,必须等待输入的重新赋值后才能进行下一次输出,在输出等待前,唤醒输入的notify(),自己再 wait() 永远等待,直到被唤醒
生产者消费问题:
生产者消费者问题是一个非常典型性的线程交互的问题。
- 使用栈来存放数据
1.1 把栈改造为支持线程安全
1.2 把栈的边界操作进行处理,当栈里的数据是0的时候,访问pull的线程就会等待。 当栈里的数据是200的时候,访问push的线程就会等待 - 提供一个生产者(Producer)线程类,生产随机大写字符压入到堆栈
- 提供一个消费者(Consumer)线程类,从堆栈中弹出字符并打印到控制台
- 提供一个测试类,使两个生产者和三个消费者线程同时运行,结果类似如下 :
栈类:
import java.util.ArrayList;
import java.util.LinkedList;
public class MyStack<T> {
LinkedList<T> values = new LinkedList<T>();
public synchronized void push(T t) {
while(values.size()>=200){
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
this.notifyAll();
values.addLast(t);
}
public synchronized T pull() {
while(values.isEmpty()){
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
this.notifyAll();
return values.removeLast();
}
public T peek() {
return values.getLast();
}
}
生产者
public class ProducerThread extends Thread{
private MyStack<Character> stack;
public ProducerThread(MyStack<Character> stack,String name){
super(name);
this.stack =stack;
}
public void run(){
while(true){
char c = randomChar();
System.out.println(this.getName()+" 压入: " + c);
stack.push(c);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public char randomChar(){
return (char) (Math.random()*(‘Z‘+1-‘A‘) + ‘A‘);
}
}
消费者
public class ConsumerThread extends Thread{
private MyStack<Character> stack;
public ConsumerThread(MyStack<Character> stack,String name){
super(name);
this.stack =stack;
}
public void run(){
while(true){
char c = stack.pull();
System.out.println(this.getName()+" 弹出: " + c);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public char randomChar(){
return (char) (Math.random()*(‘Z‘+1-‘A‘) + ‘A‘);
}
}
public class TestThread {
public static void main(String[] args) {
MyStack<Character> stack = new MyStack<>();
new ProducerThread(stack, "Producer1").start();
new ProducerThread(stack, "Producer2").start();
new ConsumerThread(stack, "Consumer1").start();
new ConsumerThread(stack, "Consumer2").start();
new ConsumerThread(stack, "Consumer3").start();
}
}
七、线程池:
7.1 什么是线程池
? 线程池是一种多线程处理形式,在多线程的场景下,频繁的创建线程和结束线程,会造成资源浪费,为了解决这个问题,引入线程池这种设计思想。
7.2、设计思路
线程池思路和生产者消费者模型很相似。
-
准备一个任务容器
-
一次性启动10个消费者线程
-
刚开始任务容器都是空的,所以线程都在wait
-
当外部线程往这个任务容器中扔了一个任务,就会有一个消费者线程被唤醒notify
-
这个消费者线程取出任务,并且执行这个任务,执行完毕后,继续等待下一次任务的到来
-
如果短时间内,有较多的任务进来,那么就有多个线程被唤醒,去执行这些任务。
整个过程中,都不需要创建新的线程,而是循环使用已存在的线程
package multiplethread;
import java.util.LinkedList;
public class ThreadPool {
// 线程池大小
int threadPoolSize;
// 任务容器
LinkedList<Runnable> tasks = new LinkedList<Runnable>();
// 试图消费任务的线程
public ThreadPool() {
threadPoolSize = 10;
// 启动10个任务消费者线程
synchronized (tasks) {
for (int i = 0; i < threadPoolSize; i++) {
new TaskConsumeThread("任务消费者线程 " + i).start();
}
}
}
public void add(Runnable r) {
synchronized (tasks) {
tasks.add(r);
// 唤醒等待的任务消费者线程
tasks.notifyAll();
}
}
class TaskConsumeThread extends Thread {
public TaskConsumeThread(String name) {
super(name);
}
Runnable task;
public void run() {
System.out.println("启动: " + this.getName());
while (true) {
synchronized (tasks) {
while (tasks.isEmpty()) {
try {
tasks.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
task = tasks.removeLast();
// 允许添加任务的线程可以继续添加任务
tasks.notifyAll();
}
System.out.println(this.getName() + " 获取到任务,并执行");
task.run();
}
}
}
}
八、原子性操作
概念:
所谓原子性操作就是不可中断的操作,比如赋值:int i=5;
原子性本身是线程安全的,但是i--
这个行为是有三个原子性操作组成:
step1:取i的值
step2:i-1
step3:把新的值赋予i
这三步都是线程安全的,但是合在一起,就不安全了。比如:
当余票i只剩1张时i=1
,有两个用户线程A,B来买票,
当A执行第一步和第二步的时候,还没将值i=0
赋予i。
用户B取到了i的值i=1
,这样结果会发生卖出了两张票的情况。
这就是产生线程安全问题的原理。
九:多线程高频面试问题
1、多线程有几种实现方案,分别是哪几种?
- 继承 Thread 类
- 实现 Runnable 接口
- 通过线程池,实现 Callable 接口
2、同步有几种方式,分别是什么,并分别说出其同步锁对象?
- 同步代码块 ==> 同步锁对象为:任意对象
- 同步方法 ==> 同步锁对象为:this
- 静态同步方法 ==> 同步锁对象为:本类名.class
3、启动一个线程时 run() 还是 star() , 说说他们的区别?
- star():用来启动线程,并调用线程中的 run() 方法
- run():执行该线程对象要执行的任务
4、sleep() 和 wait() 方法的区别?
- sleep():不释放锁对象,释放 CPU 使用权,在休眠的时间内不能唤醒
- wait():释放锁对象,释放 CPU 使用权,在等待时间内,能唤醒
5、为什么 wait()、notify()、notifyAll() 等方法都定义在 Object 类中
- 因为这些方法在使用的时候要注明所属的锁,而锁又是任意对象,所以这些方法是定义在 Object 类中的
原文:https://www.cnblogs.com/feixiong1/p/14649365.html