一、NEW 、 RUNNABLE 、 TERMINATED 状态的转换
使用isAlive()方法验证
public class demo7 {
public static void main(String[] args) {
Thread t=new Thread(()->{
for (int i = 0; i < 1000_000; i++) {
}
},"林夕");
System.out.println(t.getName()+"线程未运行之前"+t.getState());
t.start();
while (t.isAlive()){
System.out.println(t.getName()+"线程正在运行"+t.getState());
}
System.out.println(t.getName()+"线程运行结束"+t.getState());
}
}
二、WAITING 、 BLOCKED 、 TIMED_WAITING 状态的转换
public class demo8 {
public static void main(String[] args) {
final Object object=new Object();
Thread t1=new Thread(()->{
synchronized (object){
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"t1");
t1.start();
Thread t2=new Thread(()->{
synchronized (object){
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"t2");
t2.start();
}
}
使用 jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED
public class demo9 {
public static void main(String[] args) {
final Object object=new Object();
Thread t1=new Thread(()->{
synchronized (object){
while(true){
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"t1");
t1.start();
Thread t2=new Thread(()->{
synchronized (object){
while (true){
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"t2");
t2.start();
}
}
使用 jconsole 可以看到 t1 的状态是 WAITING
- BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知.
- TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒
三、yield(),可以让出cpu的调度
public static void main(String[] args) {
final Object object=new Object();
Thread t1=new Thread(()->{
while(true){
System.out.println("李白");
}
},"t1");
Thread t2=new Thread(()->{
while (true){
System.out.println("杜甫");
Thread.yield();
}
},"t2");
t1.start();
t2.start();
}
不使用yield的时候李白和杜甫大概是五五开,使用yield的时候杜甫出现的数量会明显超出李白的数量
四、线程安全
某个代码在多线程环境下会出现bug
根本原因是:
- 线程在操作系统中,随机调度,抢占式运行。
- 多个线程,同时修改同一个变量(读取是没问题的),如果是一个线程就没有问题
- 修改操作,不是"原子"的。
不可分割的最小单位;
cpu的视角:一条指令,就是cpu上不可分割的最小单位
cpu在执行调度切换线程的时候,势必会确保执行完毕一条完整的指令(原子)
原子性:我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入 房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性 的。 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进 不来了。这样就保证了这段代码的原子性了。
可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到
public class demo11 {
public static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
count++;
}
}
});
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
正常来说count的运行结果应该是100000
但实际的运行结果却未达到这个值,这个是因为++在cpu指令中不是"原子",而是多步操作。
count++背后的三个指令分别是
- load
- add
- save

这三个指令可能在运行的时候进行来回穿插,导致一次++的不完整性。
Java中提供了synchronized()关键字来完成枷锁操作
synchronized()是关键字不是函数,()中需要的是锁对象
这个对象可以是任何对象
当多个线程对同一个对象上锁时,其他的线程就会进入锁阻塞。
但如果只对一个上锁另一个线程还是会执行失败


锁对象的作用就是来区分两个线程,多个线程是否针对同一个对象枷锁
是针对同一个对象枷锁,此时候就会出现"阻塞"
不是针对同一个对象枷锁,此时不会出现阻塞,两个线程仍是随即调度的并发执行
public class demo11 {
public static int count=0;
public static void main(String[] args) throws InterruptedException {
Object lock=new Object();
Object lock2=new Object();
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 50000; i++) {
count++;
}
}
}
});
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 50000; i++) {
count++;
}
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
五、synchronized关键字
1.synchronized的特性
1.1互斥:
synchronized会起到互斥的作用,当两个线程执行到同一个对象的时候,后执行到的线程就会进入阻塞等待,等到另一个线程执行完毕之后这个线程才会继续执行。
- 进入synchronized修饰的代码块,相当于加锁
- 退出synchronized修饰的代码块,相当于解锁
synchronized用的锁是存在于Java对象里的
一个线程先上了锁,其他线程只能等待这个线程释放,
这个就像卫生间只有一个,陷进去的人锁了门,其他人再着急也只能排队。
1.2、刷新内存
synchronized的工作过程:
- 获得互斥锁
- 从主内存靠北变量的最新副本到工作的内存
- 执行代码
- 将更新后的共享变量的值刷新到主内存
- 释放互斥锁
所以 synchronized 也能保证内存可见性.
1.3、可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
把自己锁死:
一个线程没有释放锁,然后又尝试再次加锁
//第一次枷锁成功
lock();
//第二次枷锁失败进入阻塞等待。
lock();
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第 二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无 法进行解锁操作. 这时候就会 死锁

这样的锁成为不可重入锁
代码示例:
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.
- 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取 到锁, 并让计数器自增.
- 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
2、synchronized使用示例
2.1直接修饰普通方法:锁的synchronizedDemo对象
public class synchronizedDemo{
public synchronized void methond(){
}
}
2.2修饰静态方法:锁的synchronizedDemo类的对象
public class synchronizedDemo{
public synchronized void static methond(){
}
}
2.3修饰代码块:明确指定锁哪一个对象
锁当前对象
public class synchronizedDemo{
public void methond(){
synchronized (this){
}
}
}
锁类对象
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
3Java标准库中的线程安全
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
但是还有一些是线程安全的. 使用了一些锁机制来控制.
- Vector (不推荐使用)
- HashTable (不推荐使用)
- ConcurrentHashMap
- StringBuffer
注:StringBuffer 的核心方法都带有synchronized .
还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的。例如 String