类的线程安全表现为: 操作的原子性,类似数据库事务。 内存的可见性,当前线程修改后其他线程立马可看到。 不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全。
安全策略有如下三种: 1. 栈封闭 栈封闭指的是变量都是在方法内部声明的,这些变量都处于栈封闭状态。在这个独立空间创使用则绝对是安全的,它会随方法的结束而结束。
类没有任何成员变量,只有一堆成员函数,这样绝对是安全的。
Java中不管是String对象跟基本类型装箱后的对象都是不可变的,都带有final
。让状态不可变,除了加final
还可以不提供任何set方法也能做到(但这无法阻止反射机制)。
保证类的可见性,用volatile
修饰的变量在get
的时候多线程情况下不用加锁,保证可见性。但是在set的时候要加锁或者通过CAS操作进行变化。
比如ConcurrentHashMap
:
类中持有的成员变量,特别是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去(return出去),在并发情况下会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。 解决方法:用concurrentLinkedQueue等线程安全容器或者返回一个副本:
public class UnsafePublishTest {
//要么用线程的容器替换,要么发布出去的时候,提供副本,深度拷贝
private List<Integer> list = new ArrayList<>(3);
public UnsafePublish() {
list.add(1);
list.add(2);
list.add(3);
}
//将list不安全的发布出去了
public List<Integer> getList() {
return list;
}
//也是安全的, 加了锁
public synchronized int getList(int index) {
return list.get(index);
}
public synchronized void set(int index,int val) {
list.set(index, val);
}
}
竞争的资源一定是多于1个,同时小于等于竞争的线程数,资源只有一个,只会产生激烈的竞争。 死锁的根本成因:获取锁的顺序不一致,导致相互等待。
public class NormalDeadLockTest {
private static Object valueFirst = new Object();//第一个锁
private static Object valueSecond = new Object();//第二个锁
//先拿第一个锁,再拿第二个锁
private static void fisrtToSecond() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (valueFirst) {
System.out.println(threadName + " 获得第一个");
TimeUnit.MILLISECONDS.sleep(100);
synchronized (valueSecond) {
System.out.println(threadName + " 获得第二个");
}
}
}
//先拿第二个锁,再拿第一个锁
private static void SecondToFisrt() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (valueSecond) {
System.out.println(threadName + " 获得第二个");
TimeUnit.MILLISECONDS.sleep(100);
synchronized (valueFirst) {
System.out.println(threadName + " 获得第一个");
}
}
}
//执行先拿第二个锁,再拿第一个锁
private static class TestThread extends Thread {
private String name;
public TestThread(String name) {
this.name = name;
}
public void run() {
Thread.currentThread().setName(name);
try {
SecondToFisrt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread.currentThread().setName("TestDeadLock");
TestThread testThread = new TestThread("SubTestThread");
testThread.start();
try {
fisrtToSecond();//先拿第一个锁,再拿第二个锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如果怀疑发送死锁:
jps
查询应用的id
,jstack id
查看应用的锁的持有情况
简单来说就是 甲拿着A锁,获取B锁,乙拿着B锁获取A锁,注意在甲乙获得第一个锁的时候休眠会儿,来制造死锁。 解决方法:保证加锁的顺序性。 1.先锁小的再锁大的。可以通过唯一ID活着通过System自带的获得ID函数System.identityHashCode():
public class SafeOperate implements ITransfer {
//加时赛锁
private static Object tieLock = new Object();
@Override
public void transfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException {
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
// 或者你可以保证 ID唯一可以用ID实现
//始终先锁hash小的那个
if(fromHash<toHash) {
synchronized (from){
System.out.println(Thread.currentThread().getName() +" get"+from.getName());
Thread.sleep(100);
synchronized (to){
System.out.println(Thread.currentThread().getName() +" get"+to.getName());
from.flyMoney(amount);
to.addMoney(amount);
}
}
}else if(toHash<fromHash) {
synchronized (to){
System.out.println(Thread.currentThread().getName() +" get"+to.getName());
Thread.sleep(100);
synchronized (from){
System.out.println(Thread.currentThread().getName() +" get"+from.getName());
from.flyMoney(amount);
to.addMoney(amount);
}
}
}else {//解决hash冲突的方法
synchronized (tieLock) { //那个线程拿到再处理
synchronized (from) {
synchronized (to) {
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
}
}
}
2.通过tryLock 核心思路就是while死循环获得两个锁,都获得才可以进行操作然后break。
public void transfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException {
Random r = new Random();
while(true) {
if(from.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName() +" get "+from.getName());
if(to.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName() +" get "+to.getName());
//两把锁都拿到了
from.flyMoney(amount);
to.addMoney(amount);
break;
}finally {
to.getLock().unlock();
}
}
}finally {
from.getLock().unlock();
}
}
SleepTools.ms(r.nextInt(10)); // 防止发生活锁!
}
}
活锁
尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。比如上面的while循环如果没有时间休眠的话,可能会导致获取和释放锁产生时间差,来不及去处理,就可能出现想回等待的死锁:
甲拿到A尝试拿B,拿B失败了再重新尝试拿A,再重新拿B,这样周而复始的尝试。
乙拿到B尝试拿A,拿A失败了再重新尝试拿B,再重新拿A,这样周而复始的尝试。
解决办法:把对象加锁顺序的不确定性变成确定性的顺序。 解决:
线程饥饿 饥饿:线程因无法访问所需资源而无法执行下去的情况。 在CPU繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程饥饿;持有锁的线程,如果执行的时间过长,也可能导致饥饿问题。
性能 多线程是好但是要切记勿装逼强行使用,装逼必被打。使用多线程是为了提供系统的性能,充分利用系统资源。但是引入多线程后会引入额外的开销。衡量应用程序性能一般:服务时间、延迟时间、吞吐量、可伸缩性,深入了解性能优化。 做应用的时候:
先保证程序的正确性跟健壮性,确实达不到性能要求再想如何提速。 一定要以测试为基准。 一个程序中串行的部分永远是有的. 装逼利器:阿姆达尔定律 S=1/(1-a+a/n)
系统中某一部件因为采用更快的实现后,整个系统性能的提高与该部分的使用频率或者在总运行时间中比例有关。直观地,你把一个部件提升了很多,但是这个部件却不经常使用,因此这种提高看上去是提高其实并没有。所以Amdahl定律认为我们除了需要关注部件的加速比,还要关注该部件的使用频率/情况。