juc-02-评价标准

多线程-评价标准

安全性(必备条件):不损坏对象

对象损坏是一种比喻,实际上,对象是内存上的一种虚拟事物,并不会实际损坏。对象损坏是指对象的状态和设计者的意愿不一致, 通常是指对象的字段的值并非预期值。

如果一个类即使被多个线程同时使用,也可确保安全性,那么这个类就称为线程安全(thread-safe)类。由于类库中还存在非线程安全的类,所有在多线程的程序中
需要特别注意。比如:java.util.Vector类是线程安全的类,而java.util.ArrayList则是非线程安全的类。

  • 线程安全和线程兼容:虽然ArrayList是非线程安全的,当通过执行适当的互斥处理,也可用安全使用。
  • synchronized和线程安全:某个线程是线程安全的还是非线程安全的,与这个类的方法是否synchronized方法无关。

生存性(必备条件):必要的处理能够被执行

生存性(liveness)是无论是什么时候,必要的处理都一定能够被执行。

即使对象没有损坏,也不代表程序就一定好。极端一点说,假如程序在运行过程中突然停止了,这时,由于处理已经停止,对象的状态就不会发生变化了,所以对象状态
也就不会异常。这虽然符合前面将的“安全性”条件,当无法运行的程序根本没有任何意义。无论是什么时候,必要处理都一定能够被执行。

有时候安全性和生存性是相互制约的。例如,有时只重视安全性,生存性就会下降。最典型的是死锁(deadlock),即多个线程相互等待对方释放锁的情形。

可复用性(提升质量): 类可重复利用

类如果能够作为组件从正常运行的软件中分割出来,就说明这个类有很高定复用性。

编写多线程的时候如果能够巧妙地将线程的互斥机制和方针隐藏到类中,那这就是一个可复用性高的程序。

性能(提升质量): 快速,大批量地执行处理

  • 吞吐量:单位时间内完成的处理数量。能完成的处理越多,则吞吐量越大。
  • 响应性(等待时间):从发出请求到收到响应的时间。
  • 容量:可同时进行的处理数量

其他

  • 效率:
  • 可伸缩性:
  • 降级:

juc-1

多线程-入门知识

线程

概念

线程:我们追踪程序运行的流程,其实就是在追踪线程的流程。

单线程: “在某一时间点执行的处理只有一个”。(在研究单线程的时,我们忽略Java的后台线程,如:GC等)

多线程:常见的多线程常见有以下几个。

  • 耗时的IO处理:
  • 多个客户端:如果让服务端针对不同的客户端执行处理,程序是很复杂的。 客户端连接到服务器时,为客户端准备一个线程,这样看来服务器程序好像只处理一个客户端。

ps:nio即便不使用多线程,也可用执行兼具性能和可扩展性的IO处理。

“本线程” & “当前线程”

  • 本线程: 表示this(以及this对应的线程)的意思
  • 当前线程: 表示调用对象方法的线程

顺序 并行 并发

  • 顺序:依次处理
  • 并行:同时处理
  • 并发: 将一个操作分隔成多个部分并且允许无序处理

img.png

启动

两种方式

其他方式

-

java.util.concurrent.ThreadFactory利用ThreadFactory启动线程

无论那种方式,启动新线程的方法最终都是Thread类的start方法。

ps:Thread类本身还实现Runnable接口,并持有run方法,但run()主体是空的。仍需要子类重写。

线程类的实例 & 线程

需要注意的是线程类的实例并不等于线程。线程就算停止,线程类的实例并不会消失。

暂停(休眠)

在实际程序中,使用sleep的频率并不高。
Sleep的例子

  • interrupt方法:用于中途唤醒被Thread.sleep休眠的线程

互斥

线程A和线程B相互竞争引起与预期相反的情况称为竞态条件(race condition)或数据竞争(data race)。

处理上述的竞态的操作称为互斥。

synchronized方法(同步方法)

synchronized例子

图解synchronized

图解synchronized

synchronized代码块

synchronized代码块用于精准控制互斥处理的执行范围。

synchronized实例方法和synchronized代码块(this加锁)

以下两种写法是等效的。也就是说synchronizedui实例方法是使用this的锁来执行线程的互斥处理的。

1
2
3
synchronized void method(){
...
}
1
2
3
4
5
6
void method(){
synchronized(this){
....
}
}

synchronized静态方法和synchronized代码块(类对象加锁)

synchronized静态方法每次只能由一个线程运行,这一点和synchronized实例方法相同。
但是,synchronized静态方法使用的锁和synchronized实例方法使用的锁是不一样的。

1
2
3
4
5
class Something{
static synchronized void method(){
...
}
}
1
2
3
4
5
6
7
8
9
class Something{
static void method(){
synchronized(Something.class){
...
}
}
}
也就是说,synchronized静态方法是使用该类的类对象的锁来执行线程的互斥处理的。
Something.class是Something类对应的java.lang.class类的实例。

协作

wait方法,notify方法,notifyAll方法(都是java.lang.Object类的方法,所以既不是Thread类中的方法,但又是)

注意:
它们只能由持有要调用的实例的锁的线程调用。如果未持有锁的线程调用上面方法,异常java.lang.IllegalMonitorStateException会抛出。

  • 为什么放在java.lang.Object包? 这三个方法与其说是针对线程的操作,不如说是针对实例的等待队列的操作。由于所有的实例都有等待队列。它们确实
    不是Thread包的方法,但Object是所有类的父类,所以也可用说它们是Thread类的方法。

等待队列(线程的休息室)

所有实例都拥有一个等待队列,它是在实例的wait方法执行后停止操作的线程的队列。

线程退出等待队列

等待队列是一个虚拟的概念, 既不是实例中的字段,也不是用于获取正在实例上等待的线程的列表的方法。

  • 其他线程的notify或notifyAll方法来唤醒线程
  • 其他线程的interrupt方法流唤醒线程
  • wait方法超时

wait方法:线程放入等待队列

要执行wait方法,线程必须持有锁(这是规则)。如果线程进入等待队列之后,便会释放其实例的锁。

1
2
wait();
this.wait(); //等同于上面, 这里可以说线程正在this上wait

图解wait过程

notify方法

要执行notify方法,线程也必须持有调用的实例的锁。

1
2
obj.notify();
那么obj的等待队列中的线程便会被选中和唤醒,然后就会退出等待队列。
  • notify唤醒的线程并不会在执行notify的一瞬间重新运行。因为在执行notify的那一瞬间,执行notify的线程还持有着锁,所以其他
    线程还无法获取这个实例的锁。

  • 执行notify后如何选择线程:假如在执行notify方法之后,正在等待队列中等待的线程不止一个,对于“这时该如何来选择线程”这个
    问题规范中并没有做出规定。究竟是选择最先wait的线程,还是随机选择,或采取其他方法取决于Java平台运行环境。因此编写程序时需要
    注意,最好不要编写依赖于所选线程的程序。

图解notify过程

notifyAll方法

notifyAll方法会将等待队列中所有的线程都取出来。

1
2
3
notifyAll();
this.notifyAll();

notify和notifyAll的区别

  • 锁在谁的手里? 被唤醒的线程会去获取其他线程在进入wait状态时释放的锁。其实是执行notifyAll的线程正持有着锁。因此,唤醒的线程虽然都推出了
    等待队列,但都在等待获取锁,处于阻塞状态。只有在执行notifyAll的线程释放锁以后,其中的一个幸运儿才能够实际运行。

-

使用notify方法还是notifyAll方法?由于notify唤醒的线程较少,所以处理速度要比使用notifyAll快。但使用notify时,如果处理不好,程序便
可能会停止。一般来说,使用notifyAll时的代码会比notify时更为健壮。

线程的状态转移

参考下图的线程状态转移:

线程的简单状态转移