《Java多线程编程 深入详解》读书笔记
第一章 多进程多线程概述
- 线程概述
- 进程(PROCESS):CPU的执行路径
- 多进程:系统中同时运行多个程序
- 线程(THREAD):运行在进程当中的运行单元
- 多线程:每个进程里面有多个独立的或者相互有协作关系的运行单元
第二章 多线程详解
-
线程创建
-
Thread 和 Runnable 的区别与联系
Thread:负责线程本身的功能
Runnable:专注于业务逻辑的实现
两者职责功能不同,不具有可比性
Runnable中的run方法,即使不在Thread中使用,他在其他地方照样也能使用,并不能说Runnable就是一个线程
-
线程的状态:
- 初始化(被创建)、运行状态、冻结状态、终止状态(死亡)
-
其他信息:
- Main 函数本身就是一个线程,即主线程
- JVM 本身有很多后台线程在运行
- 示例:比较
extends Thread
与implements Runnable
创建线程方式- 需求描述:排队叫号系统,若10人排队,当前有3个窗口服务在服务
// 思路一:多线程处理,启动三个线程,分别处理同一个问题
/**
* 排队叫号系统;假设当前10人排队,共3个窗口工作
*/
public class TicketWindowTest extends Thread{
private int maxPersonCount ; // 最大排队人数
@Override
public void run(){
while(true){
if(maxPersonCount > 10){ // 最多10人,超过不处理
System.out.println("stop service");
break ;
}
System.out.println(Thread.currentThread().getName()+"-"+maxPersonCount++);
}
}
/**
* @param args
*/
public static void main(String[] args) {
TicketWindowTest thread = new TicketWindowTest();
TicketWindowTest thread1 = new TicketWindowTest();
TicketWindowTest thread2 = new TicketWindowTest();
thread.start();
thread1.start();
thread2.start();
}
}
// console
Thread-0-0
Thread-1-0
..........
// 分析:从运行结果中可以得出会出现同一个人被两个或多个窗口同时或重复叫号的问题
// 改进:将maxPersonCount 改为 类的属性,为该类创建的所有对象共享
// 思路一:劣势是同一段业务逻辑被不同的线程多次执行
// 思路二:只建立一个线程,启动三次
// 运行时抛出 IllegalThreadStateException 异常
// 从 Thread 类中的 start 方法可以得出一个线程只能被启动一次
// 此思路不可行
- 为何同一个线程多次启动会出现异常?参见:Thread 的 start()
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
}
// 思路三:使用Runnable ,Runnable 的作用,将业务逻辑与线程执行进行分离;且类只允许单继承,但可以多实现,所以实现多线程时建议使用 Runnable 接口;
public class TicketWindowRunnableTest implements Runnable {
private static int maxPersonCount ;
public void run() {
while(true){
if(maxPersonCount > 10){
break ;
}
System.out.println(Thread.currentThread().getName()+"-"+maxPersonCount++);
}
}
}
public class TicketWindowRunnableTestMain {
/**
* @param args
*/
public static void main(String[] args) {
TicketWindowRunnableTest test = new TicketWindowRunnableTest();
Thread thread1 = new Thread(test);
Thread thread2 = new Thread(test);
Thread thread3 = new Thread(test);
thread1.start();
thread2.start();
thread3.start();
}
}
// 从运行结果上看,只有一个线程在运行,其他没有;没实现多线程?否,将 maxPersonCount 设置为全局变量,为多个对象共享;由于线程的运行机制,有JVM决定何时运行,可能导致每次运行结果均不一致,Thread-0 执行完毕前,Thread-1 是否已启动并运行
Thread-0-0
Thread-0-1
Thread-0-2
Thread-0-3
Thread-0-4
Thread-0-5
Thread-0-6
Thread-0-7
Thread-1-8
Thread-0-9
Thread-1-10
- Runnable就是一个可执行任务的标识而已,仅此而已;而Thread才是线程所有API的体现
第三章 线程的同步
示例:多线程之间的数据共享引发的安全问题
// 将上面思路三中
public class TicketWindowRunnableTest implements Runnable {
private int maxPersonCount ; // 去掉 static 修饰
}
// 运行后输出结果
Thread-1-0
Thread-0-1
Thread-2-1
Thread-0-2
Thread-1-2
Thread-2-2
Thread-2-3
Thread-1-4
Thread-0-3
Thread-0-5
Thread-2-6
Thread-1-5
Thread-1-7
Thread-2-7
Thread-0-7
Thread-0-9
Thread-2-8
Thread-1-9
Thread-2-11
Thread-1-10
Thread-0-12
- 运行结果分析:
- 整个执行流程分为多个步骤,判断、自增(分为两个步骤 s++ ⇒ s = s + 1 ,再获取 s 的值)、输出;由于CPU的时间片轮寻,可能每个线程都没有执行完毕整个流程,中途就有其他的线程插入进来进行执行,所以导致数据获取时线程安全问题;被访问的数据没有被保护起来,而引起数据共享的安全问题
- 为何出现超过上限的值?
- 假设max_value = 9 ,三个线程依次编号为1,2,3 ;三个线程都获取到 max_value = 9 ,都通过了判断条件;线程 1 通过判断条件,进行 max_value ++ ,在进行输出之前时间片轮询;线程 2 开始执行,max_value ++ 并输出 11,再次执行判断,不满足条件,线程 2 运行结束;时间片轮询切换回到线程 1 ,输出 10;线程3,max++并输出 12 ,再次判断退出,线程 3运行结束
- 为何出现重复值?
- 初始化值为0,线程并发执行,都拿到了此时的0值,进行 max_value ++ 并输出
第一节 同步代码块
- 关键字:synchronized
public class TicketWindowRunnableTest implements Runnable {
private int maxPersonCount ;
private Object obj = new Object(); // 任意对象
public void run() {
while(true){
synchronized (obj) { // 同步代码块
if(maxPersonCount > 10){
break ;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-"+maxPersonCount++);
}
}
}
}
// 运行输出结果:不会再出现重复号码或超过最大限度的编号
- 对比思路三的实现,尽量避免使用全局变量 static ,static 修饰的变量为类所有,生命周期长,占用内存空间,得不到GC的回收
- synchronized 代码块包含的内容,表示被线程互斥访问,应尽量短小,只包含会出现安全问题的代码;
- synchronized 同步代码块:同步代码块最小的粒度应该放在共享数据的上下文,或者说共享数据被操作的上下文中
- 加锁的规则
- 加锁的目的在于防止多个线程同时对同一份数据进行操作,如果多线程运行过程中操作的数据都是各自线程的,就没有必要加锁
- 线程共享数据的锁必须是同一份的,这样才会起到线程中互相争抢锁资源的效果;反之,若局部方法内部定义锁对象,每次线程运行都会生成新的锁对象,就不会出现锁竞争,也就起不到锁的效果
- 锁对象可以是任意对象,该对象可以不参与任务计算;只要保证对于正在访问的多个线程来说此对象是唯一的
第二节 同步方法
- 示例
// 将 synchronized 加到 多线程 访问的共享的数据上
public class TicketWindowRunnableTest implements Runnable {
private int maxPersonCount ;
private Object obj = new Object();
public void run() {
while(true){
if(ticket()){
break ;
}
}
}
public boolean ticket(){
synchronized (obj) {
if(maxPersonCount > 10){
return true ;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-"+maxPersonCount++);
}
return false ;
}
}
- 为什么不在
run()
方法上添加synchronized
关键字修饰?- CPU时分复用性,多线程间切换时间片运行,JVM分配时间片给某线程,当该线程获得CPU后,加锁,运行
run()
方法;此时其他线程同样想要执行,但即使获取了CPU,未获取到锁,所以只能等待;直到该线程运行结束释放了锁,其他线程才可以执行,如上面的例子,其他线程获取了锁,run()
方法中的判断条件已经不满足了,直接退出
- CPU时分复用性,多线程间切换时间片运行,JVM分配时间片给某线程,当该线程获得CPU后,加锁,运行
- 线程同步总结:当同一份数据被多线程操作时才考虑同步
- 访问的是共享数据
- 多线程并发访问
- 线程同步的代码块必须是同一个锁
第三节 this
锁与static
锁
- 代码块的锁:对象锁,如上述示例,在 synchronized 外声明任务的对象 object
- 同一个类中不同方法间的锁:this 锁
/**
* 假设类中方法间不是 this 锁
* 那么启动 thread1 与 thread2 时,methodA 与 methodB 是可以同时执行的
* 所以,类中方法间是 this 锁
*/
public class ThreadTest1 {
public synchronized void methodA(){
System.out.println("methodA run");
while(true){
// 死循环
// 若此处使用其他方法,如 i++<50 ,起不到效果,会出现两个方法都有结果数据
// 但是,是methodA执行完毕后再执行的methodB
}
}
public synchronized void methodB(){
System.out.println("methodB run");
while(true){
}
}
/**
* @param args
*/
public static void main(String[] args) {
final ThreadTest1 test = new ThreadTest1();
Thread thread1 = new Thread(new Runnable(){
public void run() {
test.methodA();
}
});
Thread thread2 = new Thread(new Runnable(){
public void run() {
test.methodB();
}
});
thread1.start();
thread2.start();
}
}
/**
* obj 是代码块间的锁
* 不同方法间需使用 this 锁
* ticket() 使用 obj 对象锁
* ticketMethod() 使用 synchronized 修饰方法,this 锁
* 运行结果:maxPersonCount 出现 11 ,大于 10
* 理论上不可能出现 11 ,即没有起到数据同步的作用
*/
public class TicketWindowRunnableTest implements Runnable {
private int maxPersonCount ;
private Object obj = new Object();
private boolean flag = true ;
public void run() {
if(flag){
while(true){
if(ticket()){
break ;
}
}
}else{
while(true){
if(ticketMethod()){
break ;
}
}
}
}
public void change() throws InterruptedException{
Thread.sleep(3);
flag = false ;
}
public synchronized boolean ticketMethod(){
if(maxPersonCount > 10){
return true ;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-"+maxPersonCount++);
return false ;
}
public boolean ticket(){
synchronized (obj) {
if(maxPersonCount > 10){
return true ;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-"+maxPersonCount++);
}
return false ;
}
}
public class TicketWindowRunnableTestMain {
/**
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
TicketWindowRunnableTest test = new TicketWindowRunnableTest();
Thread thread1 = new Thread(test);
Thread thread2 = new Thread(test);
Thread thread3 = new Thread(test);
thread1.start();
test.change();
thread2.start();
thread3.start();
}
}
// 将上述示例中的
synchronized(object) 改为 synchronized(this) ,解决上述的问题,maxPersonCount 不会出现 11的值
// 方法间应使用 this 锁来同步数据
- 为什么Java匿名内部类的方法中用到的局部变量都必须定义为final?见上例中 new Runnable (){} 调用 test 对象的方法,强制将 test 对象使用 fianl 修饰
- 为了延长对象的生命周期,主线程main方法执行完毕后,局部变量就会运行结束等待GC,而main中的其他线程如果未执行且需要使用此局部变量,那么就需要延长生命周期
- 内部匿名类访问外部类中的局部变量,若是引用类型,由于其引用值不变,保证内部类与外部类访问的永远都是同一个内容
- static 锁
/**
* 类中方法带有 static 关键字修饰时,加锁使用类锁
* 该类创建的所有对象,共享该类锁
*/
public class TicketWindowStaticRunnable implements Runnable {
private static int max_value = 0 ;
private boolean flag = true ;
public void run() {
if(flag){
while(true){
synchronized (TicketWindowStaticRunnable.class) {
if(max_value > 10){
break ;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"_"+max_value++);
}
}
}else{
while(true){
if(ticket()){
break ;
}
}
}
}
public synchronized static boolean ticket(){
if(max_value > 10){
return true ;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"_"+max_value++);
return false ;
}
public void change() throws InterruptedException{
Thread.sleep(3);
flag = false ;
}
}
public class TicketWindowStaticRunnableMain {
/**
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
TicketWindowStaticRunnable test = new TicketWindowStaticRunnable();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
test.change();
t2.start();
}
}
- 静态锁,锁是类的字节码信息,因此如果一个类的函数为静态方法,那么我们需要通过该类的class信息进行加锁
参考资料
- 《Java多线程编程 深入详解》 - Version1.0 - 汪文君