本篇大体围绕六个部分梳理多线程相关知识
一、理解多线程
二、实现多线程操作
三、线程的状态
四、线程调度
五、守护线程
六、线程同步
一、理解多线程
早期使用的DOS操作系统为单任务的操作系统,而现在我们使用的操作系统都是多任务的操作系统,多任务操作是指在同一时刻可以做多件事情,即同时执行多个程序。
- 多进程:每一个程序都是一个进程,在操作系统中可 以同时执行多个程序,多进程的目的就是有效的利用CPU资源,每开一个进程系统要为其分配相关的系统(内存)资源。
- 多线程:线程是进程内部的比进程更小的执行单元,每个线程完成一个任务。每个进程内部包含了多个线程,每个线程完成自己的事情,在进程中的所有线程都共享该进程的资源。
- 主线程:在进程中至少存在一个主线程,其他子线程都是由主线程开启的。主线程一定在其他线程执行结束后结束,也有可能在其前结束。Java中的主线程都是main线程,就是Java的main函数。
二、实现多线程操作
Java实现多线程的方式有四种,以下一一说明:
- 继承Thread类来实现多线程:
当一个类继承Thread类后,该类就成为一个线程类,该类成为一个独立的执行单元。该类必须重写run方法,run方法由Thread类定义的,线程代码需要些在run()中。
run方法中的代码为线程代码,在run方法不可以直接调用,若直接调用,其实并没有开启一个新的线程,而是把run方法交给了调用的线程来执行。
要开启新的线程需要调用Thread类中的start方法,该方法自动开启一个新的线程并且执行run()中的内容。
public class Demo1 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class Test {
public static void main(String[] args) {
//创建Demo类的对象
Demo1 demo1 = new Demo1();
//启动线程
demo1.start();//开启一个子线程(默认名字为Thread-0(0_n))
//此时一共有三个线程
new Demo1().start();
new Demo1().start();
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
- 实现Runnable实现多线程类:
实现Runnable接口后,该类就成为一个可被线程执行的类。将实现Runnable接口的类的对象传到Thread类对象中,就可以启动线程并自动执行run()中的内容。
Runnable接口中只有run方法,调用实现该接口的类的对象无法实现启动线程
public class Demo2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class Test {
public static void main(String[] args) {
Demo2 demo2 = new Demo2();
//创建一个Thread类对象并将实现了Runnable接口的类的对象传入
Thread t = new Thread(demo2);
//启动线程,此时会自动调用run方法执行
t.start();
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
-
实现Callable接口实现多线程:
Callable和Runnable一样,实现该接口后,该类就成为一个可被线程执行的类。
区别在于:
(1)Runnable中的run方法不会抛出异常 Callable中的call方法可以抛出异常
(2)Runnable通过run方法来创建线程任务,且该方法执行后无返回结果,如果要结果需要使用全局变量获得。
Callable通过call方法来创建线程任务,该方法执行后可直接返回一个由泛型指定类型的结果
(3)Runnable可以直接通过Thread对象来执行线程任务
实现Callable的线程任务需要通过FutureTask或ExecutorService来执行线程任务FutureTask类是Future接口的一个实现类 Future表示异步计算的未来结果
Future中提供了5个方法:
(1)cancel(boolean ):用于停止线程执行,如果任务可以正常停止返回true,如果任务已完成或不能停止返回false
(2)get():获得线程任务执行后的结果(针对Callable任务),如果未完成则进入堵塞状态
(3)get(long timeout,TimeUnit unit):在指定时间内获得任务结果
(4)isDone():判断任务是否完成,如果完成返回true
(5)isCancelled():判断任务是否取消,如果在完成前取消返回true
public class Demo3 implements Callable<Integer>{
@Override
public Integer call() throws Exception{
int sum=0;
for (int i = 0; i < 100; i++) {
sum+=i;
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.sleep(100);
}
return sum;
}
}
public class Test{
public static void main(String[] args) throws ExecutionException, InterruptedException{
//创建Demo3线程任务对象
Demo3 demo3 = new Demo3();
//创建FutureTask对象
FutureTask futureTask = new FutureTask(demo3);
Thread thread = new Thread(futureTask);
thread.start();
for(int i=0;i<100;i++){
if(i==20){
futureTask.cancel(true);//取消线程任务
break;
}
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.sleep(100);
}
if(futureTask.isCancelled()){//判断线程是否取消
System.out.println("任务已取消....");
}
//线程未完成则进入等待
while(!futureTask.isDone()){
System.out.println("等待任务执行完成...");
//获得线程任务执行结果,如果未完成进入堵塞状态
System.out.println("结果为:"+futureTask.get());
System.out.println("任务完成,获得结果...");
Thread.sleep(100);
}
}
}
- 使用线程池实现多线程:
Executor框架在java.util.cocurrent 包下,它的内部使用了线程池的机制,通过该框架来控制线程的启动、执行和关闭。它提供了一种标准的方法将任务的提交过程和执行过程解耦开发(基于生产者-消费者模式)。
用Runnable、Callable来表示任务。
Executor框架相关API:
(1)Executor:接口,执行提交的Runnable任务,不显示的进行线程创建
(2)ExecutorService:接口,继承Executor接口,使用较广。提供了生命周期管理的方法,返回一个Future对象。一般使用该接口来实现和管理多线程。
ExecutorService中常用方法:
Submit:将线程任务(Runnable或Callable任务)提交给线程池中的一个线程执行,返回一个Future对象
Shutdown():关闭线程池,依次关闭线程池中提交的任务,不再接受新任务
(3)Executors:该类提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口
newCachedThreadPool():创建一个带有缓冲区的线程池,可根据需要创建新线程,在执行线程任务时先检测线程池是否存在可用线程,如果存在则直接使用,如果不存在则创建新线程然后使用。如果线程池中的线程空闲时间到达60秒后自动回收该线程。(适于执行短期线程任务)
newFixedThreadPool():创建一个固定数量的线程池,线程池中的线程数量固定,如果没有空闲线程则新任务处于等待状态,空闲线程不会被回收。(适用于执行长期线程任务)
newScheduledThreadPool():创建一个调度型线程池,可以执行定时任务
SingleThreadExecutor():创建一个只有一个线程的线程池
public class PoolDemo implements Callable<String>{
@Override
public String call() throws Exception{
for (int i = 0; i < 100; i++){
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.sleep(100);
}
return Thread.currentThread().getName()+"成功执行...";
}
}
带有缓冲区的线程池实现:
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
//提交线程任务,并返回线程处理对象
Future<String> future = executorService.submit(new PoolDemo());
}
executorService.shutdown();//关闭线程池
固定数量线程池实现:
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
//提交线程任务,并返回线程处理对象
Future<String> future = executorService.submit(new PoolDemo());
}
executorService.shutdown();//关闭线程池
调度型线程池的实现:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
RunnableTask task = new RunnableTask();
/***
* 启动定时任务
* 参数1:线程任务
* 参数2:延迟时间
* 参数3:间隔时间
* 参数4:时间单位
*/
ScheduledFuture sf = scheduledExecutorService.scheduleAtFixedRate(task,0,1000, TimeUnit.MILLISECONDS);
while (true){
System.out.println(task.getNum()+"------------");
if(task.getNum()==10){
sf.cancel(true);//取消定时任务线程
break;
}
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(sf.isCancelled()){
System.out.println("取消成功!");
}else{
System.out.println("取消失败");
}
scheduledExecutorService.shutdown();
(4)自定义线程池
通过ThreadPoolExecutor类来自定义线程池
三、线程的状态
1.新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。此时其已经有了相应的内存空间和其他资源。
2.就绪:当一个线程对象创建后,其他线程调用了它的start()方法,该线程就进入了就绪状态,该状态下的线程位于可运行池(线程池)中,等待获得CPU的使用权
3.运行:处于该状态的线程占用CPU,执行程序代码,只有处于就绪状态的线程才有机会转到运行状态
4.阻塞:有四种原因的阻塞,其线程放弃CPU资源
- 位于对象等待池中的阻塞状态:当线程处于运行状态时,如果执行了某个对象的wait()方法,JVM就会将该线程放入等待池中。
- 位于对象锁池中的阻塞状态:当线程处于运行状态,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,JVM就会把这个线程放到这个对象的锁池中。
- 执行了sleep(int millsecond)方法
- 调用了其他线程join方法
5.死亡:run()结束 ,线程死亡
四、线程调度
1.wait:线程等待
2.notify:唤醒一个处于等待的线程
3.notifyAll:唤醒所有处于等待的线程
4.sleep:线程休眠,让指定线程休眠一定的时间后自动苏醒,该方法一旦调用则线程进入堵塞状态,放弃CPU资源。当休眠时间到达后自动进入到就绪状态等待获取CPU资源
5.join:等待线程结束,在一个线程中调用了另一个线程join方法后,则该线程就自动进入到等待状态,等待另一个线程执行结束后在执行
6.yield:线程让步,中止本次线程的执行,让给其他线程执行,自己回到就绪状态继续抢占CPU资源
7.设置线程的优先级:可以通过设置线程的优先级来提高线程的执行概率,优先级越高则获得到CPU资源的概率越高,但不一定每次都能获得到执行机会
线程优先级:10为最高级(MAX_PRIORITY),1为最低级(Thread.MIN_PRIORITY),默认为5(NORM_PRIORITY)
可以通过线程的setPriority方法设置线程的优先级
五、守护线程
在所有线程中守护线程是最后结束的。一般守护线程做监控统计等功能,当其他线程都执行结束后,守护线程自动结束,如果没有执行完会被JVM强行终止
六、线程同步
6.1 线程同步的基本概念
为什么使用线程同步:
在并发环境下,当多个线程同时访问某个共享资源时可能会出现数据异常或死锁等现象。这时使用线程同步来解决这些问题。线程同步是指在访问共享资源时多个线程相互间的协调和控制,已避免数据异常。
线程同步的目的: 实现多线程对共享资源有序可控访问,保障共享资源数据的安全、避免死锁使整个系统能正常运行
实现线程同步的方式: 1.互斥 2.协作
注意:在并行程序设计中,共享资源越多,程序设计将会越复杂,所以应尽可能的减少共享资源的使用
6.2 线程的互斥同步
当有多个并行线程执行时,由于线程占用和放弃CPU资源在微观上无法预知的,所以对于共享数据的插入、删除、更新等操作,若不采取一定措施,所取得的数据很有可能不正确。
为了避免这种情况的发生,Java提供了一种同步机制的办法来解决——互斥,使用synchronized关键字<同步关键字>(加锁的方式)来实现.
互斥指临界资源(共享资源)同时只允许一个线程对其进行访问,具有唯一性和排它性,保证了线程对临界资源访问的原子性。
synchronized关键字的作用是:确保在同一时刻只有一个线程执行特定的代码块,即实现代码块的原子性。
使用Synchronized实现线程同步的两种方式:
1.同步代码块
2.同步方法(实例方法和类方法)
线程同步的实现——同步代码块
synchronized(obj){
要实现同步的代码
}
- 被synchronized包围的代码块称为同步代码块;
- synchronized代码块需要接收一个参数,该参数是一个对象,可以称为该代码块的锁对象(又称同步锁),在同步代码块中的锁对象是一个唯一的任意对象
- 任何线程持有锁对象时,方可执行该锁对象锁定的代码块;
- 如果某线程一旦持有锁对象,其它线程即不能同时持有该锁对象,从而实现线程基于特定锁对象的互斥。
注意:线程持有占锁对象并不影响其它线程访问该锁对象的方法和属性。 - 多个线程以竞争的方式争夺锁对象的监视器,线程执行完该代码块,即释放锁对象的监视器
public class Demo4 {
private String str;
//创建了一个唯一的锁对象
private static Object lock = new Object();
public Demo4(String str){
this.str = str;
}
public void p(){
//同步代码块
//当本类的不同对象在多个线程中使用时同步锁不能是this
//因为this对象不唯一
synchronized(this){
for(int i=0;i<str.length();i++){
System.out.println(str.charAt(i));
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
}
public class Demo4Test{
Demo4 d1 =new Demo4("111111");
Demo4 d2 =new Demo4("aaaaaa");
//线程1打印d1内容
new Thread(new Runnable(){
@Override
public void run(){
d1.p();//调用打印方法打印数据
}
}).start();
//线程2打印d2内容
new Thread(new Runnable(){
@Override
public void run(){
d2.p();
}
}).start();
}
}
线程同步的实现——同步方法
当同步锁对象是当前对象(使用this作为锁对象),并且要同步的代码块是整个方法体时就可以使用同步方法实现,即在方法声明的位置加入synchronized关键字
同步方法的同步锁是this
线程同步的实现——同步静态方法
在静态方法中加入synchronized关键字
使用多线程模拟多个售票点销售火车票(经典案例)
public class SaleTicket{
private int ticket;//车票数量
public int getTicket() {
return ticket;
}
public void setTicket(int ticket) {
this.ticket = ticket;
}
//售票方法
public synchronized void saleTicket() throws InterruptedException {
if (ticket <= 0) {
System.out.println("票已售完...");
return;
}
System.out.println(Thread.currentThread().getName() + "销售第" + ticket + "张票!");
Thread.sleep(50);//线程休眠
ticket--;//车票数量减少1张
}
}
public class SaleTicketWindow {
public static void main(String[] args) {
//创建售票类对象
SaleTicket ticket = new SaleTicket();
ticket.setTicket(10);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
try {
//售票操作
ticket.saleTicket();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"窗口1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
try {
ticket.saleTicket();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"窗口2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
try {
ticket.saleTicket();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"窗口3");
t1.start();
t2.start();
t3.start();
}
}
6.3 死锁
当两线程循环依赖于一对同步对象时可能发生死锁
死锁很少发生,一旦发生就很难调试
public class 死锁 {
private static final Object lock1 = new Object();//锁1
private static final Object lock2 = new Object();//锁1
public static void fn1(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":等待获得锁1...");
synchronized (lock1){
System.out.println(Thread.currentThread().getName()+":获得锁1,等待锁2...");
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+":获得锁2,开始工作...");
}
System.out.println(Thread.currentThread().getName()+":释放锁2...");
}
System.out.println(Thread.currentThread().getName()+":释放锁1...");
}
public static void fn2(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":等待获得锁2...");
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+":获得锁2,等待锁1...");
synchronized (lock1){
System.out.println(Thread.currentThread().getName()+":获得锁1,开始工作...");
}
System.out.println(Thread.currentThread().getName()+":释放锁1...");
}
System.out.println(Thread.currentThread().getName()+":释放锁2...");
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
fn1();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
fn2();
}
},"t2");
t1.start();
t2.start();
}
}
6.4 线程协作
- 线程协作是在多线程互斥同步的基础上,使线程之间依照一定条件,有目的、有计划地交互协同工作,这是一种较高级的线程同步方式。
- Java中精心设计的线程间通信机制即wait-notify机制,通过该机制可以实现线程协作。
- wait-notify机制是通过使用wait()、notify()和notifyAll()三个方法来实现的。
1.这三个方法均定义在Object类中,是final修饰的实例方法。
2.这三个方法必须在synchronized代码中调用,而且只有锁对象才能调用这三个方法。即持有锁对象监视器的线程才能调用锁对象的这三个方法 - wait()方法
1.调用该方法的线程放弃同步锁并进入等待状态(堵塞状态),直到其他线程获得相同的同步锁并调用notify( )或notify()方法来唤醒。 - notify( )
1.通知在相同同步锁上处于等待的一个线程结束等待,进入到就绪状态。
2.即唤醒一个等待(当前线程所持有)同步锁的线程。 - notifyAll()
1.通知在相同同步锁上处于等待的所有线程结束等待,进入到就绪状态。
2.即唤醒所有等待(当前线程所持有)同步锁的线程。
生产者消费者模型(经典案例)
生产者消费者模型使用了线程等待唤醒机制,来实现生产者和消费者之间的协作
生产者生成商品,消费者消费商品,通过两个之间的协作达到生产消费的平衡
//面包类
public class Bread {
private int num;
public Bread(int num) {
this.num = num;
}
}
//面包柜类
public class BreadContains {
//声明一个数组用于存储面包对象
private Bread[] breadContains;
private int size = 0;//面包柜中的面包数量
public BreadContains(int initSize) {
this.breadContains = new Bread[initSize];
}
//向面包柜中添加面包的方法
public synchronized void add(Bread bread) {
while(size == breadContains.length){//判断面包柜是否已满
System.out.println(Thread.currentThread().getName()+":面包柜已满,抓紧卖面包...");
try {
/***
* 当一个线程调用了wait方法后,则该线程被挂起(其后的代码不在执行),此时会记录当前线程的挂起点
* 当前线程一旦被唤醒并获得CPU资源后从从挂起点继续执行
*/
wait();//调用当前相等的等待方法,让该线程进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//面包柜未满,放入面包
breadContains[size] = bread;
size++;//数量加1
System.out.println(Thread.currentThread().getName()+":放入第"+size+"个面包!");
notifyAll();//唤醒在当前同步锁上等待的所有线程,如果没有等待的线程则什么都不做
}
//从面包柜中移除一个面包
public synchronized void remove() {
//判断面包柜中是否有面包
while(size==0){
System.out.println(Thread.currentThread().getName()+":面包柜已空,抓紧生产面包....");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":卖出第"+size+"个面包!");
//从面包柜中取面包
size--;
Bread bread = breadContains[size];
notifyAll();//唤醒在当前同步锁上等待的线程
}
//测试类
public class Test {
public static void main(String[] args) {
//初始化面包柜,容量为10
BreadContains contains = new BreadContains(10);
//面包师1线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//创建一个面包对象(生成一个面包)
Bread bread = new Bread(i);
contains.add(bread);
Thread.yield();//线程让步
}
}
},"面包师1").start();
// 销售1员线程销售面包
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
contains.remove();
Thread.yield();
}
}
}, "销售员1").start();
//面包师2线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//创建一个面包对象(生成一个面包)
Bread bread = new Bread(i);
contains.add(bread);
Thread.yield();//线程让步
}
}
},"面包师2").start();
//销售员2线程销售面包
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
contains.remove();
Thread.yield();
}
}
}, "销售员2").start();
}
}