并发编程
查看进程和线程
linux
进程
ps -fe //查看java进程
ps -fe | gropu java //使用管道运算符筛选java进程
jps //jdk自带命令,查看进程windows也可用
kill pidxxx //杀掉进程
线程
top -H -p pidxxx //查看进程下的所有线程
jstack pidxxx //抓取某一刻的线程信息
栈与栈帧
main线程运行时内存图,栈是线程私有的,所以此处只给线程main分配一个栈
线程上下文切换
即多个线程轮流使用cpu,每次切换,线程计数器都要记录他的线程执行到那一步了。
常见方法
join
join方法一般用于线程间通信 ,**底层原理是wait **
main{
t.join(); //在main线程中调用t的join,也就是说当t运行结束后main就运行
}
main{
t1.join();
t2.join();
//当t1和t2中运行最慢的结束后可以运行,t1和t2可以并行运行
}
等待的时间已时间运行的时间为准
main{
t.join(10); //如果t运行只花了3s,那么main线程3s后就会运行,如果运行时间超过10s,例如15s,那么就mian就在10s后运行
}
interrept
1、打断睡眠的线程和正常的线程
非睡眠状态下打断线程后打断标志为true,但线程继续运行
2、两阶段中断异常
3、isInterrupted()和interrupted()
前者不会清除打断标志,后者会清除
4、park
LockSupport.park();//当前线程阻塞 ,当使用interrupt打断时可以继续运行,打断标记为正时park失效
主线程与守护线程
一般情况下当线程运行结束后进程才会结束,对于守护线程来说当其他线程运行结束后只剩守护线程时进程也会结束
习题:统筹规划
第四章:并发之共享模型
同步与互斥
互斥保证了操作的原子性,同步等价于线程间的通信
共享带来的问题
为什么需要共享cpu
无法利用cpu的情况:sleep,wait,io操作,通过分时系统提交cpu的利用率。即一个时间段内多个线程可以获取cpu资源
java中的体现
问题的产生:线程一对静态变量i自加5000次,线程二对静态变量i自减5000次,最后的i不是0.
问题分析:i++操作在字节码层面不是一条指令。分为多条指令进行,在运算时在线程内进行运算然后写到主存,所线程一中计算后的结果还没有写到主存中cpu时间片用完了,线程二读取到了未被计算的i,所以造成这个结果
临界区
一段代码块内如果存在对共享资源的多线程读写操作,这段代码块称为临界区
竞太条件
多个线程在临界区内执行,由于代码的执行序列不同导致结果无法预测,称之发生了竞态条件
解决方法
阻塞式:synchronized,lock
非阻塞式:原子变量
互斥是对于同一资源来说的(静态变量i),同步只是规定了线程的执行顺序
synchronized保证了操作的原子性,但并不保证可见性???
synchronized
在修改了本地内存中的变量后,解锁前会将本地内存修改的内容刷新到主内存中,确保了共享变量的值是最新的,也就保证了可见性。
方法上的synchronized
变量线程安全分析
方法本身是线程私有的,安不安全是对于这个方法里操作的资源来说的,如果这个资源是静态变量或者是引用对象等有可能造成线程不安全的问题
成员变量的安全性
局部变量的安全性
子类继承造成的线程安全问题
当子类覆盖了父类的方法后父类的局部变量会暴漏,方法的访问修饰符是有意义的,可以在一定程度上保护方法中的资源安全问题
常见线程类
String,Integer,StringBuffer,Random,HashTable,concurrent包下的类。
也就是说他们的每个方法是原子的,但是组合在一起不一定是线程安全的
String类为什么是不可变类
线程安全类方法的组合
实例分析
售票问题
转账问题
使用的锁必须是A账户和B账户同一把锁,所以将锁加在方法上是不行的,要用Account.class。使用类锁只能有两个账户同时转账,如果账户很多时会造成性能的低下
Monitor概念
java对象头
Integer对象的大小为8+4=12字节
monitor在操作系统中叫做管程
锁
轻量级锁
如果一个资源有多个线程访问,但多线程访问的时间是错开的(无竞争),如果产生了竞争会升级为重量级锁。
语法任然是synchronized
加锁过程
调用加锁方法时,线程的栈帧里产生一个锁记录对象(JVM层面),
锁记录中的Object reference指向锁对象。
通过ca操作替换锁对象(Object)的mark word,将mark word的值存入锁记录,mark woed的值就变为了锁记录的地址和状态码。锁记录地址用来表示那个线程
如果cas失败有两种情况
1、其他线程已经拥有了这个对象锁,表明有竞争,进入锁膨胀
2、如果原来的线程自己执行了synchronized锁重入,方法栈中新建一个栈帧里面有锁记录对象,发现object标记是00,即轻量级锁,然后查看到所记录的地址是同在一个线程,不会发生锁膨胀。
升级为重量级锁
自旋锁
自旋是为了让后来的线程一尝试获取锁,而不是让他进入阻塞状态。适合多核cpu。如果重试多次后不成功则进入阻塞状态。jdk7后不能控制是否开启自旋
偏向锁
偏向锁在对轻量锁的优化在于每次锁重入不需要进行cas操作,CAS是cpu的一条指令。具体操作是将线程id设置到对象的mark word头,而不是轻量级锁中的锁记录对象地址 适用于冲突很少的情况,当线程很多时可以禁用偏向锁。 当一个对象调用hashcode时会禁用偏向锁。hash码填充到markword中。
wait
必须和synchronized配合使用
waiting是已经获得过锁但是又放弃的锁。wait必须先成为owner才能wait
blocked是没有获得到锁
notify唤醒的是waitset里的线程
同步模式之保护性暂停
join的实现,future的实现就是采用的此模式
任务:
1、使用此模式在下载时与join的区别,一个等结果,一个等结束
2、设置超时等待,要防止虚假唤醒
3、join就是使用了保护性暂停模式
4、多任务版guardObject常用于RPC
异步模式之生产者消费者
park和unpark
1、wait,notify必须匹配monitor使用
2、以线程为单位,即可以精确的唤醒某个线程,而notify只能随机唤醒一个线程
3、可以先执行unpark,执行多次的结果和执行一次的效果是一样的,但是notify却不能
状态转换
runnable<-- -->waitting
使用join,wait线程会进入waitset中,当notify,或者interrupt时会竞争cpr资源,如果成功则变为runnable,不成功则变为blocked
死锁、活锁、饥饿
死锁:一个线程互相拥有另一个线程想要的锁,导致两个线程阻塞
活锁:两个线程都在使用cpu,但由于互相改变结束的条件,导致都不能结束
饥饿:一个线程很少获得cpu资源
ReentrantLock
1、synchronized不可以中断,此锁可以。
2、可以设置超时时间。
3、可以设置公平锁,synchronized时非公平的,在entrylist中的谁竞争到谁先执行
4、可以设置多个条件变量,即有多个waitset,根据不同的情况选择进入哪一个,synchronize只有一个waitset
5、都支持可重入
应用
1、三种方式固定线程输出顺序(wait,lock,park)
2、三个线程循环五次输出abc。park操作的是线程