《Java并发编程的艺术》3.Java并发编程基础

Java并发编程基础

1.1线程简介

线程作为操作系统调度的最小单元。

现代操作系统在运行一个程序时,为其创建一个进程,例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时运行。

线程优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配,线程分配到的时间片也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

在Java线程中,使用一个整型成员变量Priority来控制优先级,优先级的范围从1-10,在线程构建的时候可以通过 setPriority(int k)来修改优先级。默认优先级是5优先级高的线程分配时间片的数量要多于优先级较低的线程,设置线程的优先级时,针对频繁阻塞的(休眠后者I/O操作)的线程需要设置较高的优先级。而偏重计算(需要较多CPU时间片或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。

注意

线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定

1.2线程的状态

Java线程在运行的生命周期中可能处于下表中的6中不同的状态,在给定的一个时刻,线程只能处于其中的一个状态。(Thread类的一个枚举类 State定义的6中状态)

状态名称 说明
NEW 初始状态,线程被构建,但是还没有调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪运行两种状态笼统的称作 ‘“运行中”’
BOLCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一个特定动作**(通知(唤醒)或者中断)**
TIME_WAITING 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的。
TERMINATED 终止状态,表示当前线程已经执行完毕

状态转换示意图

在这里插入图片描述

1.3理解线程中断

参考

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrsupted()对当前线程的中断标识位进行复位,如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()方法将会返回false。

1.4线程间通信

1.4.1volatile和synchronized关键字

关于volatilesynchronized具体参考

关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回主内存(共享内存),它能保证所有线程对变量的可见行。

synchronized对于同步代码块底层使用了monitorenter指令和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。

1.5等待/通知机制(非Lock)

等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object里。

等待通知的相关方法

方法名称 描述
notify() 通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁
notifyAll() 通知所有等待在该对象上的线程
wait() 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或中断才会返回,需要注意,调用wait()方法后,会释放对象的锁
wait(long) 超时等待一段时间,这里的参数是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
wait(long, int) 对于超时时间更细粒度的控制,可以达到纳秒。

等待/通知机制,是指一个线程A调用了对象O的**wait()**方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作,上述两个线程通过对象O来完成交互,而对象上的wait()和notify()/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

注意事项

  • 使用wait()、notify()、notifyAll()时需要先对调用对象加锁**(即必须在同步方法或者同步代码块中)**
  • 调用wait()方法后,线程状态由RUNNABLE变为WAITING,并将当前线程放到对象的等待队列中
  • notify()/notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()/notifyAll的线程释放锁之后,等待线程才有机会从wait()返回(被唤醒)
  • notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。
  • 从wait()方法返回(被唤醒)的前提是获得了调用对象的锁

从上述细节可以看到,等待 / 通知机制依赖于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改

注意:同步队列与等待队列不是一个东西,同步队列是只要获得锁就能出队,而等待队列是需要被唤醒才有资格进入同步队列去抢锁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5cIHfmIA-1637214715538)(img/WaitNotify运行过程.png)]

1.6等待/通知的经典范式

通过一些经典的等待/通知案例,我们可以提炼出等待/通知的经典范式,该范式分为两部分,分别针对等待方(消费方)通知方(生产者)。

等待方遵循以下原则

1.获取对象的锁
2.如果条件不满足,那么调用对象的wait()方法,被通知后仍然要检查条件(while循环)
3.条件满足则执行对应的逻辑

-- 对应的伪代码
synchronized(对象){
	while(条件不满足){
	    对象.wait();
	}
	对应的处理逻辑
}

通知方遵循如下原则

1.获取对象的锁
2.改变条件
3.通知所有等待在对象上的线程

-- 对应的伪代码如下
 synchronized(对象){
 	   改变条件
 	   对象.notifyAll();
 } 

1.7线程池简单原理

public class DefaultThreadPool<Job extends Runnable> implents ThreadPool<Job>{
    
    
    
//这时一个工作列表,将会向里面插入工作 Job就是一个Runnable,一个任务
private final LinkedList<Job> jobs = new LinkedList();

//工作者列表
private final List<Worker> workers = Collections.synchronizedList(new AraryList(Worker));
    
}
  • 当客户端调用execute(Job)时,会不断地向任务列表jobs中添加job,而每个工作者线程会不断的从jobs上取出一个job进行执行,当job为空时,工作者线程进入等待状态。
  • 添加一个job后,对工作队列jobs调用了其notify()方法,而不是notifyAll方法,因为能够确定有工作者线程被唤醒,这时使用notify()方法将会比notifyAll()方法获得更小的开销(避免将等待队列中的线程全部移动到阻塞队列中)。
  • 可以看到,线程池的本质就是使用了一个线程安全的工作队列连接工作者线程好客户端线程客户端线程将任务放到工作队列后便返回,而工作者线程则不断的从工作队列上取出工作并执行。当工作队列为空时,所有的工作者线程均等待在工作队列上,当有客户端提交了一个任务之后会通知任意一个工作者线程,随着大量的任务被提交,更多的工作者线程被唤醒

猜你喜欢

转载自blog.csdn.net/qq_46312987/article/details/121399100
今日推荐