Java 给线程引入了六种状态
线程状态 | 含义说明 |
|---|---|
NEW | 安排了工作,还未开始行动 |
RUNNABLE | 可工作的,又可以分成正在工作中和即将开始工作 |
BLOCKED | 这几个都表示排队等着其他事情 |
WAITING | 这几个都表示排队等着其他事情 |
TIMED_WAITING | 这几个都表示排队等着其他事情 |
TERMINATED | 工作完成了 |
public class Demo13 {
public static void main(String[] args) throws InterruptedException {
Thread mainThread = Thread.currentThread();
Thread t = new Thread(() -> {
while (true){
System.out.println(mainThread.getState());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
System.out.println(t.getState());
t.start();
t.join(1000);
System.out.println(t.getState());
}
}一个进程的多个线程,共享同一份内存资源,如果两个线程,都尝试修改某个变量,就可能出现冲突; 某个逻辑单个线程执行是可以的,但是多个线程执行出现问题,这就是线程不安全,反之则线程安全
eg:线程不安全例子
public class Demo14 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
//创建两个线程,分别对同一个变量进行5w次 ++ 操作
//最终主线程打印结果
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}使用两个线程分别对同一个变量进行5w次 ++ 操作,其结果应该为10w,但运行结果确像是一个<10w的随机值,这就是两个线程对同一变量修改的不安全。count++操作实际是三次指令,将内存值加载到cpu寄存器中,在cpu寄存器中对值进行计算,将寄存器再写入到内存中,由于是三次指令,可能在某一条指令时调度到别的线程,这样的调度穿插过程就可能出现线程安全问题。
锁当前对象
synchronized (obj){
代码块
}(括号中的对象:从语法角度来说可以是任何对象,只要是Object的实例即可;从语义角度来说,两个线程填写相同的对象才会有锁竞争,才会有阻塞效果。)
要点:
synchronized public void add(){
count++;
}public void add(){
synchronized(this){
count++;
}
}此时锁的this对象就是调用add方法的对象
private static void add(){
synchronized (Demo16.class){
count++;
}
}无论哪种写法,synchronized 方法针对啥对象加锁不重要,重要的是两个线程是否针对同一个对象加锁!
synchronized 对同一线程具有可重入性,不会存在把自己锁死的情况。
死锁的场景:
死锁的四个必要条件 (打破任意一个就可以避免死锁)
如何避免死锁? 打破请求和保持:在代码中尽量避免锁的嵌套 打破循环等待:约定加锁的顺序(把锁进行编号,约定任何一个线程多把锁的时候,都需要按照标号从小到大的顺序来加锁)
volatile 能够保证内存可见性
内存可见性是由于编译器优化导致的,编译器优化又是什么:举个例子,在程序员中写代码的水平是参差不齐的,而写的差占据多数因此就在编译器中加入了”优化机制“,编译器自动分析这一部分代码逻辑,保持代码逻辑不变的前提下,自动修改代码内容,让代码变得更加的高效。
eg:
public class Demo18 {
private static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0){
}
System.out.println("t1 结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入 flag 的值");
flag = scanner.nextInt();
System.out.println("t2 结束");
});
t1.start();
t2.start();
}
}解决方案:使用volatile 关键字修饰某个变量,此时编译器就知道这个变量" 易变 ",后续编译器针对这个变量的读写操作就不会涉及到优化了。
public class Demo18 {
private static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t1 结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入 flag 的值");
flag = scanner.nextInt();
System.out.println("t2 结束");
});
t1.start();
t2.start();
}
}加上sleep(1)后为什么代码又可以正确执行了,并不是sleep解决了内存可见性。而是因为本质上内存可见性是由编译器优化带来的,因为引入了sleep,这个代码中的load操作没有被编译器优化掉,因为sleep背后是非常多的指令,消耗的时间比load读取一次多得多,所以即使优化掉load操作也没什么太大作用。所以是sleep影响到了编译器优化,因此内存可见性没有了。
多线程之间是随机调度的,执行顺序难以知道,而有时我们又希望能够确定多个线程之间的先后执行顺序,而join方法只能确定线程的结束顺序,此时就需要用到wait和notify方法。除此之外,wait和notify还可以解决"线程饿死"问题。 wait(),notify(),notifyAll() 都是Object 类的方法。
wait 方法让当前线程进入等待状态;wait方法必须搭配synchronized来使用。 wait做的三件事情:
notify 方法是唤醒等待的线程。 该方法用来通知那些可能等待该对象的线程,对其发出通知并使他们重新获取该对象的锁。如果有多个线程等待,则随机挑选一个wait 状态的线程;在notify 方法后并不会马上释放该对象锁,而是要等待执行notify 方法线程将程序执行完也就是退出同步代码块才会释放对象锁。
public class Demo20 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker1){
System.out.println("t1 wait 之前");
try {
locker1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 wait 之后");
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1){
System.out.println("t2 notify 之前");
locker1.notify();
System.out.println("t2 notify 之后");
}
});
t1.start();
t2.start();
}
}notifyAll 方法:notify方法只是唤醒某⼀个等待线程. 使用notifyAll方法可以⼀次唤醒所有的等待线程。
都可以让线程阻塞,都可以指定阻塞的时间