目录
1.线程等待
2.join()介绍
3.获取当前对象引用
4.线程的状态
5.线程安全
6.synchronized()关键字
7.synchronized关键字底层介绍
1.线程等待
对于操作系统而言,内部多个线程的执行是“随机调度,抢占式执行”的。简而言之线程的等待是在确定线程的“结束顺序”。在操作系统中,虽然无法确定哪个线程执行的顺序以及执行的频率,但是可以控制哪个线程先结束。
例如现有两线程A,B;若果在A线程中调用B线程,B.join();
意思就是就是控制A线程等待B线程执行结束后再执行。哪个线程线程调用join();哪个线程就先执行。
如举例所示:在 t.start(); 之后本来应该两个线程一起执行的,但是经过 t.join();main 线程发生堵塞,只有 t 线程执行,等到 t 结束后,join才会返回,main线程接着执行。
调整一下,如果先让 t 结束,然后main才开始执行,这个时候才开始join() 是否main会堵塞呢
可以发现,如果 t 线程已经执行结束,主线程再次调用 t.join(); main线程也不会发生阻塞,因为join();就是为了确保能够先结束,如果已经在join();之前结束,join()就不必再等待了。
注意:此处仅仅是main线程等待t1,t2,但是t1,t2之间没有等待关系
总结:任何线程之间都是可以相互等待的,并不是只能主线程等待别人,线程等待并不是两个线程之间,一个线程可以等待多个线程。
2.join()介绍
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束最多等待millis毫秒 |
public void join(long millis,int nanos) | 同理,但是精度更高 |
public void join(long millis) :millis十毫秒级的时间,设置等待时间一旦超过时间就会自己结束
public void join(long millis,int nanos):一个时间的范围,nanos是更高精度的纳秒。一般精度只到ms级别,再往上计算机就很难做到了。精度越高,开销就越大。
join();方法有多种重载的方法,上一标题介绍的是无参数版本无参数就意味着没有等待时间限制,如果被等待的线程发生阻塞结束不了,那么join();就会一直等待。这是一个十分危险的操作。
用于军工领域的计算机系统“实时操作系统”,可以把调度的开销降到很低但是符合一定误差要求,从而获取更高精度。舍弃很多功能换来了高精度实时性。
3.获取当前对象引用
在某个线程中,想要获取自身的Thread对象的引用,就可以使用currentThead()方法来获取。
在其他线程中想要调用main线程,不采用currentThread();好像很难获取main线程的引用,想要获取当前线程的引用,只需要在当前线程调用currentThread即可。
任何线程中都可以通过这样的操作,拿到线程的引用,线程的终止也是通过调用当前线程的引用里的方法: Thread.currentThread.isInterruptted()
调用Thread.sleep(); 方法让线程阻塞等待,是具有一定时间的。
线程执行sleep,就会使线程不参与cpu调度,从而把cpu的资源让出来给别的操作使用。
这样的sleep操作称为“放权” 操作。在有的场景中,发现某个线程占用的cpu资源很高但是又使用的很少就可以通过sleep休息短暂的时间来改善。
线程的优先级也可以产生此影响,但是影响是有限的。通过sleep更加明显的影响到cpu占用。
4.线程的状态
java中对于线程的状态做出了更明晰的划分,不只有阻塞和就绪两种状态。
1.NEW:当你使用 new Thread()
创建了一个线程对象,但还没有调用 start()
方法时,线程处于 NEW
状态。
2.TERMINATED:当线程执行完 run()
方法,或者在 run()
方法中抛出未捕获的异常,它就会进入 TERMINATED
状态。
3.RUNNABLE:当你调用了 start()
方法后,线程就会进入 RUNNABLE
状态,它表示线程已准备就绪,等待被操作系统调度。
4.BLOCKED: 一个进程试图获取其他进程所持有的锁时,他就会处于这个状态。直到这个锁释放
5.TIMED_WAITING:有具体的时间等待
6.WAITING:没有具体的时间等待
5.线程安全
举例:多个线程同时执行一个代码的时候可能会引起一些bug,理解线程安全是解决或者避免bug的关键。
上述代码并没有正确相加得出100000,下面分析原因。
此处的count++,在cpu看来是3个指令(下面的指令在不同编译器中写法不同)
1.把内存的数据读取到cpu寄存器里:load
2.把cpu寄存器的数据+1 :add
3.把寄存器里的值写回内存 :save
但是因为是三个指令,cpu会出现只执行了其中一个或者两个,剩下的指令就会被调度走。这样就会容易出现bug。
根本原因是因为:
1.线程在操作系统中随机调度,抢占式执行,
2.多个线程同时修改一份变量
3.修改的操作不是“原子”的(原子的:不可分割的最小单位),在cpu的视角,一条指令就是不可分割的最小单位,cpu在切换线程的时候只能确保执行完一条指令。
4.内存可见性,指令重排(下节介绍)
只有第一张图中的两种方式是正确执行的,第二张图就是错误的。指令的调度有很多种顺序,除了第一张图中的两种顺序是正确的,其他任何执行顺序得到的结果都是不正确的。
如何解决上述问题?
那就要从原因下手了,线程在操作系统的随机调度抢占式执行是很难干涉的,其次如果多个线程能同时修改同一变量也是不可控制的,因为这也是操作系统多线程的特性。但是如果修改的操作不是原子的就可以。使分开执行的指令一次性执行完就好了。
比如count++中,让数据的读取,修改,写回内存都是原子性的,其他线程的指令插入不进来就可以了。
6.synchronized()关键字
这个关键字后面的()并非填的是”参数“,而是填入的是一个指定的锁对象,通过锁对象来进行判定,锁对象可以是任何对象。
{} 内部就是要一同执行的整体,在执行的时候,其他线程的代码的逻辑插入不进来。
值得注意的是,想要针对修改同一变量的线程加锁,这些线程所持有的锁对象必须一致是同一对象。不然加锁无效!!
由于t1和t2 都是针对locker对象加锁的,t1先加锁成功,t1就直接执行 {} 里的代码
t2也加锁了,但是比t1慢上一步,当t2发现对象已经被别人先锁起来了,那么t2只能等到t1 的{}执行结束释放锁后,t2再加锁。
又因释放锁unlock一定是在save之后,确保了t1的count++的结果可以正确写入内存,两者的count++不会穿插执行,也就不会覆盖掉对方的结果了。
加锁本质上是把 局部随机并发执行的代码 强行变成了串行,从而解决线程安全问题。
注意:
1.锁对象的作用是区分两个线程或者多个线程是否针对同一个对象加锁。都是同一对象的锁就会出现“阻塞”(锁竞争)。所加的锁不是同一对象那么多个线程还是并发执行。
2.锁对象必须是对象,是引用类型Object类或者其子类,不能使int,double这种内置类型。
3.加锁后代码只是局部代码穿行,但效率依然比join要快。只有锁里面的是串行,其他部分代码不影响并发执行。
7.synchronized关键字底层介绍
synchronized()关键字是jvm提供的功能,底层实现就是通过C++代码编写的,也是依靠操作系统提供的API实现的加锁,操作系统的API是来自由于cpu上支持特殊的指令实现的。
操作系统原生的API就是两个函数lock()/unlock(),大多数编程语言是类似于封装的方式来使用这两个函数,但是java中直接通过一个关键字来同时完成加锁解锁,这样的好处是在编写代码的时候最后很有可能会忘记unlock解锁,这个关键字会自动替你解锁。就算直接trturn也会帮你释放锁再return。
synchronized()里面可以是任何对象,最偷懒的写法就是直接某个类.class(类对象),但是偷懒需要付出的代价就是代码效率会降低。
一个类对象可以获取到这个类里面的详细情况,包括但不限于类有哪些属性,方法,属性是什么类型,什么名字,方法是什么类型,返回类型,这个类实现了哪些接口等等。这就是反射,反射是一组API可以对上述信息获取或者修改。
synchronized 还可以修饰一个方法:
如果修饰类方法就没有this,就是直接给类对象加锁。
注意:在多线程中并非就是写了synchronized就是安全的,还要看具体代码怎么写,是否要加synchronized是要看具体场景。
比如StringBuffer,Vector,Hashtable都不推荐,因为加了太多的锁,会导致代码效率降低。
总结:synchronized的几种使用方式:
1.synchronized(){ };圆括号指定锁对象
2.synchronized 修饰一个普通方法相当于针对this加锁。
3.synchronized 修饰一个静态方法,相当于对类对象加锁。
可以把任意Object子类或者Object类的对象作为锁对象,锁对象是什么不重要,重要的是多个线程的对象是否同一个,是同一个才会出现锁竞争。