一、基本概念:
线程同步:
线程安全:当多个线程访问某个类时,这个类都始终能表现出正确的行为,那么就称这个类是线程安全的。
线程与进程:
二、线程创建
三种方式
三、线程状态
五种状态
四、线程控制基本方法
阻塞线程的方法:
join():合并线程,当前线程会等待调用该方法的线程执行完毕后才会执行
sleep():使线程进入阻塞状态,但是不会交出锁。由于线程进入了阻塞态,即使没有其他等待执行的线程,该线程也不会获得执行。
yield():使线程进入就绪态,该方法也不会交出锁。但由于线程是进入了就绪态,如果当前没有其他执行的线程,则该线程会获得执行。
wait():
notify():
一、synchronized
1.synchronized锁的是对象。
对于代码块,synchronized(o){}锁的是括号中的对象,可以使用this锁住当前对象
对于普通方法上加synchronized关键字,锁的是该方法所在的对象,也即相当于使用synchronized(this){}
对于静态方法上加synchronized关键字,锁的是类对象。
细粒度、粗粒度???
2.synchronized的使用:
线程不安全的情况:
1 | public class Syn1 implements Runnable{ |
执行结果:
分析原因:
从Java的内存模型角度分析,各个线程拥有自己的工作区,对象存在于主存。线程工作时需要从主存中拷贝对象数据的副本,修改完成后再写入主存。
如果代码中一个线程拿到了主存中的数据100,但还没有修改,此时再发生线程切换,另一个线程也拿到了主存中的数据100,一个线程修改完成后将101
写入主存,另一个线程修改完成后同样是101,写入主存。如此一来主存中本应该变成102的数据确变成了101,数据出现了错误。即count++并不是一个原子操作。
加入synchronized关键字。
1 | public class Syn1 implements Runnable{ |
3.synchronized使用常见问题
1)同步和非同步方法可以同时调用
由于在执行非同步方法时,并不需要获得锁,故可以在对象被锁住的情况下执行。
代码示例:
1 | public class Syn2 { |
运行结果:
1 | t1m1 start |
可见m1获得锁之后,m2仍然能正常执行。
2)如果只在写方法上加锁,在读方法上不加锁,则容易产生脏读的现象(读取未提交数据)
如果在写的数据提交前后各读取一次,则可能读取的数据
3)synchronized锁可重入
一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。这样的机制避免了很多死锁情况。
同时这种情况也适用于继承的情况,子类继承父类,在子类加锁方法中调用父类加锁的方法同样可以。
1 | public class Syn4 { |
4)程序执行过程中,如果同步代码块中出现异常,锁会被释放
5)synchronized粒度问题
在保证线程安全的前提下,应尽量缩小synchronized代码块的范围,这样可以提高并发。
7)锁锁定某对象o,如果o的属性发生了改变,不会影响锁的使用,如果对象o变成了另外一个对象,则锁状态改变,原本抢到的锁作废,线程会去抢新锁。
如下列代码,执行s.o = new Object();后,t1仍然锁住的是旧对象,而t2则抢到了新对象的锁,因此两个线程都获得了各种的锁,因而都可以执行了。
1 | public class Syn5 { |
二、volatile
1.volatile的可见性
在多线程的环境中,当一个线程修改了主存中的某个数据的值,另一个线程不一定会马上知道,也就是并不一定马上会从主存中更新该数据的值(特别是
在改线程所在的cpu特别忙的情况下)。而对变量使用volatile关键字则会保证所有线程都会读到变量的修改值。
例如在以下程序中如果不加入volatile关键字,程序会一直执行下去,即使在主线程中running变量被修改为了false,但此时另一个线程并不会更新
running变量。而加了volatile关键字后,线程中的running变量会强制更新。
1 | public class Volatile1 { |
2.volatile不可保证原子性
对应count++问题,volatile并不能保证count的正确性。
1 | public class Volatile2 implements Runnable{ |
synchronized与volatile对比
三、AtomXXX类
AtomXXX类是一类在java.util.concurrent下封装好的类,其方法本身就具有原子性,且其效率比synchronized要高。但需注意其各种方法之间并不能保证原子性。
1 | public class Atom1 implements Runnable{ |
四、死锁
当多个线程各自锁定一个资源等待对方锁定的资源时会发生死锁,典型的有哲学家进餐问题。
1.死锁实例
1 | public class DeadLock { |
2.死锁避免:
- 保持获取锁的顺序一致
- 尽量使用开放调用
- 使用定时锁
3.死锁诊断
可通过JVM的线程转储信息来分析死锁。
4.其他活跃性危险
饥饿:当线程由于无法访问它所需要的资源而不能继续执行时,就发生了饥饿。常见的有线程优先级使用不当,优先级低的线程始终得不到CPU的执行时会导致该线程饥饿。我们应该尽量保持各线程优先级相同。
思考角度:
内存角度
线程重入
延迟角度
线程调度异步性