补充:之前的线程休眠 sleep ,参数是以ms作为单位,但是sleep本身就存在一些误差。sleep(1000),不一定是精确在休眠1000ms(线程的调度,也是需要时间的)
sleep(1000)的意思是说该线程在1000ms之后,就快恢复成“就绪状态”,此时只是随时去cpu执行,但是并不是立刻马上就去执行
因为sleep的特性,也诞生了一个特殊的技巧,sleep(0)让线程放弃cpu,准备下一轮的调度(一般后台开发不太涉及到这个东西,yield方法效果是和sleep(0)一样
一、💛
PCB的状态字段:
就绪状态,阻塞状态 是系统设定的状态。Java把这两个状态进行了细分。
NEW:安排了工作,但是还没有执行(类创建好了,但是没开始start),如下图,他还没有开始start.
Runnable:可工作的,又分成(1)正在工作的(线程在cpu上运行) (2)线程在这里排队,随时可以进去cpu
TERMINSTED:终止状态
BLOCKED:因为锁产生的阻塞(一会讲解锁)
WAITING:因为调用wait产生阻塞(不带时间的)(以后讲解)
TIMED_WAITING:因为sleep产生阻塞(带时间的)
二、💜
线程安全问题(重点,考点)
线程不安全:是说单线程执行下没有问题,但是多个线程的执行下,就出现问题了。
bug:实际运行效果,和预期效果不一样就可以叫做bug
count++:本质是分成三个步骤
1.把内存的数据,加载到cpu的寄存器
2.把寄存器中的数据进行+1
3.把寄存器中的数据写入内存中
而上述操作在两个线程中可能有以下几个问题
原因就是抢占式执行的结果
bug1号
bug2号,这种当然也很典型, 此外这两个线程的调度顺序也是不确定的,两个操作相对顺序也有差异,那么排序情况就有了无数种情况。
1W的循环结果里,在多少次运行,多少次出现覆盖结果,导致运算的中间结果,也被覆盖了,结果一定小于1w,很多代码都涉及线程安全问题,不仅仅是count++。
线程安全的原因:
1.多个线程之间调度顺序是随机的,操作系统使用抢占式,执行的策略来调整线程。和单线程不同的是,多线程喜爱,代码的执行顺序,产生了更多变化,以往只需考虑一个代码在一个固定顺序下执行正确即可,现在要考虑的多线程下,N种执行顺序下,代码执行结果都要正确。(跑的快和跑的对,有时候就是会冲突)
2.多个线程同时修改同一个变量,容易产生线程安全问题(代码的结构)
一个线程修改一个变量,多个线程,读取同一个变量,多个线程修改,多个变量都是ok的
3.进行的修改,不是“原子性的”(事务里面,看我之前的),如果修改操作按原子方式去完成,此时也不会有线程安全问题,
4.内存可见性,引起的线程安全问题
5.指令重排序,引起的线程安全问题
针对3号的(也是解决线程安全的最主要的操作)解决方法:加锁,相当于把一组操作包装成一个原子的操作(此处这里的原子,则是通过锁进行互斥,我这个线程工作的时候,其他线程无法进行操作。
Java引入了原子操作synchronized关键字(务必会读(辛可肉耐子)会写)
写在方法前面(进入方法就进行加锁操作,出了方法就解锁)
synchronized:只要出了代码不管你是return 也好,抛异常也好,都会正常解锁,比一般的什么lock()方便,所以lock()这种东西也不多介绍。
class Count{
public int count=0;
synchronized public void increase(){
count++;
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
Count counter = new Count();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join(); //等待t1执行完毕,之前上一个没写join,但是就算写他也是错的
t2.join(); //等待t2执行完毕,最后main线程
System.out.println(counter.count);
}
}
三、💚
那么来了一个问题,既然要串行化并发执行,那么多线程有存在的必要吗(不还是一个线程完事,去另一个线程吗?
答:有必要,我们看上面那一串代码,我们能知道线程不是只干了count++这一件事啊,我们也有循环,循环不也是我们需要做的吗,for循环并没有加锁,不涉及线程安全问题,for循环操作的变量是栈上的局部变量,两个线程有两个独立的空间,
两个i不是同一个变量,两个线程改不同的变量,当然不需要加锁了!
因此,两个线程,一部分代码串行化执行,有一部分并发式执行,仍然比纯粹的串行效率要高。
synchronized:进行加锁解锁,其实是以对象展开的
⚠️:t1占一次换t2,再换就是t1,不是5000次一直都是t1啥的哈,两个线程是同时执行的
加锁的目的是为了互斥使用资源(互斥的修改变量)
💖💖💖重点:如果两个线程针对同一个对象加锁,就会出现锁竞争/锁冲突(即会有阻塞现象)
假如是不同对象则不存在锁竞争,也就不会阻塞,那么数据也会变得不可靠。
public void increase(){ synchronized (this){ count++; } synchronized public void increase(){ count++} //这两个式子相同的意思,就是上面的可以更灵活一点,可以写不同对象
class Count{
public int count=0;
private Object locker=new Object(); //另一个对象
public void increase() {
synchronized (this) {
count++;
}
}
public void increase2(){
synchronized (locker){ //另一个对象,不存在锁竞争
count++;
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
Count counter = new Count();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter.increase2();
}
});
t1.start();
t2.start();
t1.join(); //等待t1执行完毕
t2.join(); //等待t2执行完毕,最后main线程
System.out.println(counter.count);
}
}
像是上述代码,两个对象针对不同对象加锁,此时不存在阻塞等待,也就不会让程序按照串行的方式进行count++,也就仍然会有线程安全问题,既然不阻塞等待,那么也就是说会有刚才那种的覆盖情况