多线程(九)并发安全

多线程(九)并发安全

1.CompletionService

1.1为什么要提出

如果任务有返回值 我们会使用Future包装获取其返回值,当有多个返回值时我们就需要一个容器来存储这些Future

并且更重要的是CompletionService可以先拿到先执行完成任务返回的结果。

//工作线程
public class WorkTask implements Callable<Integer> {
    private String name;
    public WorkTask(String name) {
        this.name = name;
    }

    @Override
    public Integer call() {
        //休眠随机的时间
        int sleepTime = new Random().nextInt(1000);
        try {
            Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 将上面随机的休眠时间 返回给调用者
        return sleepTime;
    }
}
public class CompletionCase {
    //获取当前机器CPU的核心数
    private final int POOL_SIZE = Runtime.getRuntime().availableProcessors();
    private final int TOTAL_TASK = Runtime.getRuntime().availableProcessors();

    // 方法一,自己写集合来实现获取线程池中任务的返回结果
    public void testByQueue() throws Exception {
    	long start = System.currentTimeMillis();
    	//统计所有任务休眠的总时长 需要在多线程环境下共享 所以需要使用原子类
    	AtomicInteger count = new AtomicInteger(0);
        // 创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(POOL_SIZE);
        //容器存放提交给线程池的任务 使用list Map都可以存放
        BlockingQueue<Future<Integer>> queue = 
        		new LinkedBlockingQueue<Future<Integer>>();

        // 向里面扔任务
        for (int i = 0; i < TOTAL_TASK; i++) {
            Future<Integer> future = pool.submit(new WorkTask("ExecTask" + i));
            queue.add(future);//按顺序进入队列 第0个先进 然后是第1个...
        }

        // 检查线程池任务执行结果
        for (int i = 0; i < TOTAL_TASK; i++) {
            //获取任务执行完成之后的返回值 从队列中取出返回值 只能按顺序取 先取第一个 再取第二个...
        	int sleptTime = queue.take().get();
        	System.out.println(" slept "+sleptTime+" ms ...");
        	//累加每次的任务的返回值
        	count.addAndGet(sleptTime);
        }

        // 关闭线程池
        pool.shutdown();
        //打印所有任务休眠的总时长(其实就是每次任务执行完成之后的返回值)
        System.out.println("-------------tasks sleep time "+count.get()
        		+"ms,and spend time "
                //打印程序的执行时间
        		+(System.currentTimeMillis()-start)+" ms");
    }

}

//测试类 先使用自己实现的容器
 public static void main(String[] args) throws Exception {
        CompletionCase t = new CompletionCase();
        t.testByQueue();
  }
//测试结果
 slept 250 ms ...
 slept 616 ms ...
 slept 469 ms ...
 slept 157 ms ...
-------------tasks sleep time 1492ms,and spend time 624 ms

1.2 上面自己实现的容器有什么问题?

进入队列时按照顺序进入,当任务执行完成后取任务的返回值也是按照顺序取的。有些任务执行速度快,完成后只能等待前面的任务都取出返回结果后才能取出这些任务的返回值。

而CompletionService容器能够实现 先完成任务的先取得返回值。

    // 方法二,通过CompletionService来实现获取线程池中任务的返回结果
    public void testByCompletion() throws Exception {
    	long start = System.currentTimeMillis();
    	AtomicInteger count = new AtomicInteger(0);
        // 创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(POOL_SIZE);
        // 关闭线程池
        pool.shutdown();
        System.out.println("-------------tasks sleep time "+count.get()
			+"ms,and spend time "
			+(System.currentTimeMillis()-start)+" ms");
    }

测试类

public static void main(String[] args) throws Exception {
        CompletionCase t = new CompletionCase();
        t.testByCompletion();
}

执行结果

 slept 34 ms ...
 slept 584 ms ...
 slept 787 ms ...
 slept 865 ms ...
-------------tasks sleep time 2270ms,and spend time 873 ms

先完成的任务不用等待其他的线程就能先拿到返回值

在线程池中线程数不变,任务数*10的情况下

private final int POOL_SIZE = Runtime.getRuntime().availableProcessors();
private final int TOTAL_TASK = Runtime.getRuntime().availableProcessors();

打印的结果可能 出现乱序的现象,这是由于任务数过多 打印掩饰等原因造成的,实质上还是先完成的任务先取到返回值。

1.3 CompletionService的实现原理

public class ExecutorCompletionService<V> implements CompletionService<V> {
    private final Executor executor;
    private final AbstractExecutorService aes;
    //阻塞队列 只有当任务完成后才能放入到该队列中 这样就能做到先完成的任务先取到返回值
    private final BlockingQueue<Future<V>> completionQueue;
}

2.并发安全

2.1类的线程安全定义

如果多线程下使用这个类,不过多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的。

类的线程安全表现为:

  • 操作的原子性:使用类中的方法进行操作的时候,这个操作不可以被中间其他操作打断,该操作要么全部成功,要么全部失败。
  • 内存的可见性:一个线程对一个变量进行修改后,其他的线程可以马上看到变量的修改。

类是对操作(方法)和状态(类中的变量)的封装

在多个线程之间共享状态的时候如果没有做正确的处理(不正确的同步),就会出现线程不安全。

如果我们不共享状态 即没有多个线程并发的修改一个变量 那么我们就认为线程安全。

2.2 怎么才能做到类的线程安全?

①栈封闭

所有的变量都是在方法内部声明的,这些变量都处于栈封闭状态。 Snipaste_20191017_211713.png

②无状态

没有任何成员变量的类,就叫无状态的类

/**
 *类说明:无状态的类,没有任何的成员变量
 */
public class StatelessClass {
	
	public int service(int a,int b) {
		return a*b;
	}
	
	//...public void t(){}
}

③让类不可变

让状态不可变,两种方式:

所有基本状态的包装类都是不可变的类(String也是不可变的)

1,加final关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上final关键字,但是加上final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。

//不可变的类
public class Immutable {
    //成员变量是私有且不可变的
	private final int a;
	private final int b;
	public ImmutableFinal(int a, int b) {
		super();
		this.a = a;
		this.b = b;
	}
	public int getA() {
		return a;
	}
	public int getB() {
		return b;
	}
}


/**
*类说明:看起来不可变的类,实际是可变的 
*/
public class ImmutableFinalRef {
	
	private final int a;
	private final int b;
	private final User user;//这里,就不能保证线程安全啦
	
	public ImmutableFinalRef(int a, int b) {
		super();
		this.a = a;
		this.b = b;
		this.user = new User(12);
	}

	public int getA() {
		return a;
	}

	public int getB() {
		return b;
	}
	
	public User getUser() {
		return user;
	}

	public static class User{
        //虽然 User被final修饰 但是它的成员变量age不是final修饰的
        //当调用getUser()方法时能够获取User的引用 进而可以修改age的值 所以是线程不安全的
		private int age;

		public User(int age) {
			super();
			this.age = age;
		}

		public int getAge() {
			return age;
		}

		public void setAge(int age) {
			this.age = age;
		}
		
	}
	
	public static void main(String[] args) {
		ImmutableFinalRef ref = new ImmutableFinalRef(12,23);
        //可以拿到对象的引用
		User u = ref.getUser();
        //通过对象的引用修改User类中的成员变量age
		u.setAge(35);
	}
}

2、根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值(如果成员变量作为方法的返回值 其他方法可以获取到该成员变量的引用 就可能修改该成员变量)

/** 
 *类说明:不可变的类 没有提供任何修改list的值的方法 所以是线程安全的
 */
public class Immutable {
	private List<Integer> list =  new ArrayList<>(3);
	
	public ImmutetableToo() {
		list.add(1);
		list.add(2);
		list.add(3);
	}
	
	public boolean isContains(int i) {
		return list.contains(i);
	}
}

④volatile

保证类的可见性,最适合一个线程写,多个线程读的情景,

⑤加锁和CAS

⑥ 安全的发布

类中持有的成员变量,特别是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。

/**
 * 不安全的发布
 */
public class UnsafePublish {
	//要么用线程的容器替换
	//要么发布出去的时候,提供副本,深度拷贝
	private List<Integer> list =  new ArrayList<>(3);
	//JVM保证初始化过程是线程安全的
	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);
	}	

}

⑦TheadLocal

PS:

Servlet 不是线程安全的类 为什么我们平时没感觉到?

  • 在需求上Servlet很少有共享的需求
  • 接收到了请求,返回应答的时候,都是由一个线程来负责的。

3.线程不安全造成的问题

3.1 死锁

3.1.1死锁的定义

  • 死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,

    若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

  • 注意事项:资源一定是多于1个,同时小于等于竞争的线程数,资源只有一个,只会产生激烈的竞争。

  • 死锁的根本成因:获取锁的顺序不一致导致。

3.1.2产生死锁的演示

主线程执行firstToSecond()方法 先获取第一个锁再获取第二个锁

SubTestThread线程执行SecondToFirst()方法 先获取第二个锁再获取第一个锁

两个线程并发执行,主线程获取第一个锁的同时 SubTestThread线程获取第二个锁

这样就造成了主线程一直等待第二个锁 而第二个锁被SubTestThread线程持有

SubTestThread线程一直等待第一个锁 而第一个锁被主线程持有 这样就陷入了一种僵局

造成的根本原因:获取锁的顺序不一致导致。

public class NormalDeadLock {
    private static Object valueFirst = new Object();//第一个锁
    private static Object valueSecond = new Object();//第二个锁

    //该方法 先拿第一个锁,再拿第二个锁
    private static void firstToSecond() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (valueFirst) {
        	System.out.println(threadName+" get first");
            //让线程有足够的时间去获取锁 如果去掉这句 不一定会发生死锁
        	SleepTools.ms(100);
        	synchronized (valueSecond) {
        		System.out.println(threadName+" get second");
			}
		}
    }

    //该方法 先拿第二个锁,再拿第一个锁
    private static void SecondToFirst() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (valueSecond) {
        	System.out.println(threadName+" get second");
            //让线程有足够的时间去获取锁 如果去掉这句 不一定会发生死锁
        	SleepTools.ms(100);
        	synchronized (valueFirst) {
        		System.out.println(threadName+" get first");
			}
		}        
    }

    //该线程 执行先拿第二个锁,再拿第一个锁
    private static class TestThread extends Thread{

        private String name;

        public TestThread(String name) {
            this.name = name;
        }

        public void run(){
            Thread.currentThread().setName(name);
            try {
                SecondToFirst();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Thread.currentThread().setName("TestDeadLock");
        TestThread testThread = new TestThread("SubTestThread");
        testThread.start();
        try {
            firstToSecond();//先拿第一个锁,再拿第二个锁
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果

TestDeadLock get first
SubTestThread get second

3.1.3使用JVM工具检测死锁的发生

命令行指令:

查看当前所有运行的进程:

jps

16064 Launcher
1188
10952 NormalDeadLock
9000 Jps
4060 KotlinCompileDaemon

查看我们当前运行的程序的栈帧:

jstack 10952
Found one Java-level deadlock: #发现一个死锁
=============================
"SubTestThread": 
  waiting to lock monitor 0x000000000304c608 (object 0x000000076b5a2a58, a java.lang.Object),
  which is held by "TestDeadLock"
"TestDeadLock":
  waiting to lock monitor 0x000000000304b0b8 (object 0x000000076b5a2a68, a java.lang.Object),
  which is held by "SubTestThread"

Java stack information for the threads listed above:
===================================================
"SubTestThread":
        at pers.amos.concurrent.ch7.NormalDeadLock.SecondToFirst(NormalDeadLock.java:34)
        #等待0x000000076b5a2a58 这个锁
        - waiting to lock <0x000000076b5a2a58> (a java.lang.Object)
        #已经持有0x000000076b5a2a68 这个锁
        - locked <0x000000076b5a2a68> (a java.lang.Object)
        at pers.amos.concurrent.ch7.NormalDeadLock.access$000(NormalDeadLock.java:11)
        at pers.amos.concurrent.ch7.NormalDeadLock$TestThread.run(NormalDeadLock.java:51)
"TestDeadLock":
        at pers.amos.concurrent.ch7.NormalDeadLock.firstToSecond(NormalDeadLock.java:22)
        #等待0x000000076b5a2a68 这个锁
        - waiting to lock <0x000000076b5a2a68> (a java.lang.Object)
        #已经持有0x000000076b5a2a58 这个锁
        - locked <0x000000076b5a2a58> (a java.lang.Object)
        at pers.amos.concurrent.ch7.NormalDeadLock.main(NormalDeadLock.java:63)

Found 1 deadlock.

3.1.4解决死锁的方案

保证加锁的顺序一致

//修改SecondToFirst()方法 让它也先获取第一个锁再获取第二个锁
private static void SecondToFirst() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (valueFirst) {
        	System.out.println(threadName+" get first");
        	//让线程有足够的时间去获取锁
        	SleepTools.ms(100);
        	synchronized (valueSecond) {
        		System.out.println(threadName+" get second");
			}
		}        
    }

执行结果

TestDeadLock get first
TestDeadLock get second
SubTestThread get first
SubTestThread get second

3.1.3 死锁的分类

死锁分为简单的死锁动态顺序的死锁

[1]简单的死锁

怀疑发送死锁:

1.通过jps 查询应用的 id。

2.再通过jstack id 查看应用的锁的持有情况。

解决办法:保证加锁的顺序性

[2]动态顺序死锁

  • 由于方法入参由外部传递而来,方法内部虽然对两个参数按照固定顺序进行加锁,但是由于外部传递时顺序的不可控,而产生锁顺序造成的死锁,即动态锁顺序死锁。
  • 动态顺序死锁,在实现时按照某种顺序加锁了,但是因为外部调用的问题,导致无法保证加锁顺序而产生的。

模拟转账操作示例

银行转账动作的接口

public interface ITransfer {
	/**
	 * 
	 * @param from 转出账户
	 * @param to 转入账户
	 * @param amount 转账金额
	 * @throws InterruptedException
	 */
    void transfer(UserAccount from, UserAccount to, int amount)
    		throws InterruptedException;
}

不安全的转账动作的实现

/**
* 表面上锁的顺序一致 都是先锁转出再锁转入
* 其实这就造成了动态顺序死锁
*/
public class TransferAccount implements ITransfer {
   
    @Override
    public void transfer(UserAccount from, UserAccount to, int amount)
          throws InterruptedException {
        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);
            }
        }
    }
}

模拟转账公司

public class PayCompany {

    /*执行转账动作的线程*/
    private static class TransferThread extends Thread {

        private String name;//线程名字
        private UserAccount from;
        private UserAccount to;
        private int amount;
        private ITransfer transfer; //实际的转账动作

        public TransferThread(String name, UserAccount from, UserAccount to,
                              int amount, ITransfer transfer) {
            this.name = name;
            this.from = from;
            this.to = to;
            this.amount = amount;
            this.transfer = transfer;
        }


        public void run() {
            Thread.currentThread().setName(name);
            try {
                transfer.transfer(from, to, amount);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        UserAccount amos = new UserAccount("amos", 20000);
        UserAccount mikami = new UserAccount("mikami", 20000);
        ITransfer transfer = new TransferAccount();
        //同时运行两个线程 进行两次转账
        TransferThread amosToMikami = new TransferThread("amosToMikami"
                , amos, mikami, 2000, transfer);
        TransferThread mikamiToAmos = new TransferThread("mikamiToAmos"
                , mikami, amos, 4000, transfer);
        amosToMikami.start();
        mikamiToAmos.start();

    }

}

结果:

amosToMikami getamos
mikamiToAmos getmikami

出现死锁的原因:

这是动态顺序死锁,虽然我们对所有线程都是先锁住转出账户再锁住转入账户 但是实际应用时

第一个线程先锁住了转出账户amos 第二个线程先锁住了转出账户mikami 这就造成了死锁。

3.1.4解决 死锁的两种方法

1、 通过内在排序,保证加锁的顺序性

2、 通过尝试拿锁,也可以。

[1]解决方法示例一:

思路:当两个用户同时转账时 我们比较它们的唯一标识(这里使用的是用户的hashCode 当然也可以使用用户的id等唯一的属性)我们总是先锁定hashCode小的那一个账户 再锁定hashCode大的那一个账户

面临的问题:

  • 用户的hashCode被重写怎么办?

    我们可以使用jdk的方法获取用户原生的hashcode:System.identityHashCode(from);

  • 两个用户的hashCode一样(发生哈希碰撞)怎么办?

    如果发生哈希碰撞 让这两个账户再竞争一次锁 谁先获得了这个锁 谁就先执行 这样就不会出现死锁了

    这就相当于NBA球赛打平时进入加时赛。

/*
 *类说明:不会产生死锁的安全转账
*/
public class SafeOperate implements ITransfer {
	private static Object tieLock = new Object();//加时赛锁

    @Override
    public void transfer(UserAccount from, UserAccount to, int amount)
            throws InterruptedException {
        //获取原生的hashCode
    	int fromHash = System.identityHashCode(from);
    	int toHash = System.identityHashCode(to);
    	//先锁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);						
					}
				}
			}
    	}
    	
    }
}

修改main方法

   public static void main(String[] args) {
        //使用安全的转账方法
        SafeOperate safeOperate = new SafeOperate();
        UserAccount amos = new UserAccount("amos", 20000);
        UserAccount mikami = new UserAccount("mikami", 20000);
        TransferThread amosToMikami = new TransferThread("amosToMikami"
                , amos, mikami, 2000, safeOperate);
        TransferThread mikamiToAmos = new TransferThread("mikamiToAmos"
                , mikami, amos, 4000, safeOperate);
        amosToMikami.start();
        mikamiToAmos.start();
   }

结果:

amosToMikami getamos
amosToMikami getmikami
mikamiToAmos getamos
mikamiToAmos getmikami

[2]解决方法示例二:

使用显示锁 尝试拿锁 当拿不到锁时 马上释放锁 将锁让给其他线程

public class SafeOperateToo implements ITransfer {
    @Override
    public void transfer(UserAccount from, UserAccount to, int amount)
            throws InterruptedException {
       //休眠随机时间 错开这两个线程的休眠的时间尽量的避免活锁
       Random r = new Random();
       while(true) {
          //from尝试获取锁
          if(from.getLock().tryLock()) {
             try {
                System.out.println(Thread.currentThread().getName()
                      +" get "+from.getName());
                //from已经获取到锁 现在to获取锁
                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));
       }
    }
}

结果:

//第一次获取锁
mikamiToAmos get mikami
//mikami第一次获取mikami锁成功了 但是获取amos锁失败了 所以释放掉了mikami锁
amosToMikami get amos
amosToMikami get mikami
mikamiToAmos get mikami
mikamiToAmos get amos

2.4 其他安全问题

活锁

尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。

解决办法:每个线程休眠随机数,错开拿锁的时间。

活锁示例:

/*SafeOperateToo类中SleepTools.ms(r.nextInt(10))方法休眠随机时间 
 *错开这两个线程的休眠的时间尽量的避免活锁
 *现在去掉这一句 我们来看运行结果
*/
//SleepTools.ms(r.nextInt(10));

//输出结果
amosToMikami get amos
amosToMikami get amos
amosToMikami get amos
amosToMikami get amos
mikamiToAmos get mikami
mikamiToAmos get mikami
mikamiToAmos get mikami
mikamiToAmos get mikami
mikamiToAmos get mikami
mikamiToAmos get mikami
amosToMikami get amos
amosToMikami get amos
amosToMikami get amos
amosToMikami get amos
mikamiToAmos get mikami
amosToMikami get amos
mikamiToAmos get mikami
amosToMikami get amos
mikamiToAmos get mikami
amosToMikami get amos
mikamiToAmos get mikami
amosToMikami get amos
mikamiToAmos get mikami
mikamiToAmos get mikami
mikamiToAmos get mikami
mikamiToAmos get mikami
mikamiToAmos get mikami
amosToMikami get amos
mikamiToAmos get mikami
mikamiToAmos get mikami
amosToMikami get amos
amosToMikami get amos
amosToMikami get amos
amosToMikami get amos
amosToMikami get amos
amosToMikami get amos


线程饥饿

低优先级的线程总是拿不到执行时间

3.性能和思考

使用并发的目标是为了提升性能,引入多线程后会引入额外的开销。

衡量应用程序的性能的指标:服务时间、延迟时间、吞吐量、可伸缩性。前两者决定程序能有多快。吞吐量决定了完成工作量的多少,是程序处理能力的指标。多块与多少是完全独立的,有时候可能是相互矛盾、完全对立的。

对服务器应用来说,多少(可伸缩性、吞吐量)这个方面比多快更加重要。所以做应用的时候我们要注意:

1.先保证程序的正确性,当确实达不到要求的时候,再去提高速度(黄金原则)。

2.一定要以测试为基准。

一个应用程序中串行的部分是永远无法避免的,我们能做的是尽量减少串行的部分。

3.1 影响性能的因素

上下文切换

当前可运行的线程大于CPU的数量时,CPU会把它的可执行时间进行分片,分给线程去运行。当线程把自己的时间片用完后,系统就需要把当前运行完成的线程从CPU中拿走,让其他线程进入,使用CPU。进行一次上下文切换要消耗5000~10000个时钟大概几微妙的时间。

解决方案:根据计算机的性能合理分配线程数。

内存同步

其实就是加锁,增加额外的指令。

阻塞

当一个线程无法获取锁或者处于IO状态时都会被挂起。挂起包括两次额外的上下文切换。

3.2 性能优化方案:减少锁的竞争

3.2.1 缩小锁的范围

对锁的持有,尽量快进快出,尽量缩短持有锁的时间。尽量不要在加锁的操作里调用sleep方法,因为sleep方法虽然让出了CPU的时间但是不会释放锁。

public class ReduceLock {
	//线程不安全 需要加锁
	private Map<String,String> matchMap = new HashMap<>();
	//传入name和正则表达式,判断map中是否含有符合正则表达式的value
	public synchronized boolean isMatch(String name,String regexp) {
		String key = "user."+name;
		String job = matchMap.get(key);
		if(job == null) {
			return false;
		}else {
			return Pattern.matches(regexp, job);
		}
	}
	
}

上面锁住了整个方法,锁的范围过大。

//只需要锁住这句就可以了 保证多线程环境下的线程安全
synchronized (this){
    String job = matchMap.get(key);
}

  • 避免多余的缩减锁的范围
synchronized (this){
   String job = matchMap.get(key);
}
   job = job + "str";//执行时间过短没必要加两次锁
synchronized (this){
	job = matchMap.get(key);
}

只加一个锁就够了

synchronized (this){
   String job = matchMap.get(key);
   job = job + "str";//执行时间过短没必要加两次锁
   job = matchMap.get(key);
}

其实JVM也有锁粗化机制,会自动检测两次加锁的时间间隔,如果时间间隔过小,会把两个锁合并为一个锁。

3.2.2 减少锁的粒度

使用锁的时候,锁所保护的是多个对象,并且这些对象其实是独立变化的,这时候就需要使用多个锁来一一保护这些对象,但是要避免发生死锁。

/**
*减小锁的粒度 
*使用FinessLock类的实例对象作为锁一次保护users和queries两个变量
*面临的问题:当一个线程想要调用addUser方法时,另一个线程先拿到了锁调用了removeQuery方法 这样添加用户的
*线程就需要等待。所以我们可以缩小锁的粒度来解决这个问题
*/
public class FinenessLock {
	//存储当前系统的用户 两者是相互独立的变量
	public final Set<String> users = new HashSet<>();
    //存储当前系统的访问
	public final Set<String> queries = new HashSet<>();
	
	public synchronized void addUser(String u) {
		users.add(u);
	}
	
	public synchronized void addQuery(String q) {
		queries.add(q);
	}
	
	public synchronized void removeUser(String u) {
		users.remove(u);
	}
	
	public synchronized void removeQuery(String q) {
		queries.remove(q);
	}	

}

解决方案:减小锁的粒度

public class FinenessLock {
	
	public final Set<String> users = new HashSet<>();
	public final Set<String> queries = new HashSet<>();
	
	public void addUser(String u) {
	    synchronized (users){
            users.add(u);
        }
	}
	
	public void addQuery(String q) {
        synchronized(queries){
            queries.add(q);
        }
	}
	
	public void removeUser(String u) {
	    synchronized (users){
            users.remove(u);
        }
	}
	
	public synchronized void removeQuery(String q) {
	    synchronized (queries){
            queries.remove(q);
        }
	}	

}


3.2.3 锁分段

ConcurrentHashMap

锁分段带来的问题:某些情况下需要访问整个容器的某个数据时,需要对整个容器进行加锁。

当使用ConcurrentHashMap的size()方法时为了防止锁住整个容器size返回的是一个估计值。

3.2.4 替换独占锁

  • 使用读写锁
  • 用自旋CAS
  • 使用系统提供的并发容器:如HashMap->ConcurrentHashMap

4.线程安全的单例模式

4.1 双重检查实现单例模式的问题与解决方案

PS:双重检查实现单例模式不推荐使用,推荐使用懒汉式和饿汉式来实现单例模式。

实现单例模式的流程

为了保证SingleDcl类的对象为单例

在对SingleDcl类进行初始化时,先检查singleDcl是否已经初始化

然后锁住SingleDcl类 这里需要再进行一次检查 确定singleDcl是否已经被实例化。

为什么需要再次检查?

当第一次检查后获取锁之前,有可能其他的线程已经获取锁完成对象的初始化并且释放了锁。

public class SingleDcl {
    private static SingleDcl singleDcl;
    private SingleDcl(){
    }

    public static SingleDcl getInstance(){
    	if (singleDcl == null){
    	    synchronized (SingleDcl.class){
    	        if (singleDcl == null){
    	            singleDcl = new SingleDcl();
                }
            }
        }
        return singleDcl;
    }
}

双重检查一定能保证线程安全吗?

假设有线程A和线程B两个线程同时获取singleDcl对象

线程A先进入:当第二次检查以后如果singleDcl == null 就去初始化SingleDcl类的对象

如果初始化的过程很复杂很耗时这就可能导致对象的域没有赋值完成 但是对象的引用已经有了即(singleDcl !=null)

线程B:在singleDcl对象的域(属性)还没有赋值完成时 获取singleDcl对象 由于singleDcl != null(因为虽然域没有赋值完成但是对象的引用已经有了)直接返回singleDcl

这样当线程B调用singleDcl的方法时可能出现异常。

public class SingleDcl{
    private int a;
    //这个域初始化很复杂 也很耗时
    private User user;
}

//线程B获取到了对象 并且调用其方法可能出现异常
singleDcl.getUser().getId();//因为user属性还没有赋值完成 直接抛出空指针异常

解决方案

public class SingleDcl {
    //添加volatile关键字 保证在获取到singleDcl对象时,该对象中所有的属性都已经初始化完成
    private volatile static SingleDcl singleDcl;
    ...
}

4.2 懒汉式实现单例模式

懒汉式也叫类初始化模式-延迟占位模式

原理:在JVM中,对类的加载和类的初始化,由虚拟机保证线程安全。但是实例初始化时,JVM是不保证线程安全的。

public class SingleInit {
    private SingleInit(){}
	//定义一个私有类来持有当前类的实例 JVM对当前类加载时是不会加载该私有类的
    private static class InstanceHolder{
        public static SingleInit instance = new SingleInit();
    }
	//当获取当前类的实例时 私有类才会返回当前类的单例返回出去 
    public static SingleInit getInstance(){
        return InstanceHolder.instance;
    }

}

懒汉式不仅可以应用于单例模式还可以应用在其他地方

懒汉式实现初始化类的部分域

//当调用构造方法的时候只初始化value
public class InstanceLazy {
	
	private Integer value;
	private Integer val ;//可能很大,如巨型数组1000000;
	
    public InstanceLazy(Integer value) {
		super();
		this.value = value;
	}

	public Integer getValue() {
		return value;
	}
	//私有类 用于初始化val的值
	private static class ValHolder {
		public static Integer vHolder = new Integer(1000000);
	}
	//当调用该方法时采取初始化val
	public Integer getVal() {
		return ValHolder.vHolder;
	}	

}

4.3 饿汉式实现单例模式

原理:在JVM中,对类的加载和类的初始化,由虚拟机保证线程安全。但是实例初始化时,JVM是不保证线程安全的。

public class SingleEHan {
    //一开始就把对象实例创建出来
    public static SingleEHan singleEHan = new SingleEHan();
    private SingleEHan(){}

}

饿汉式的问题:如果当前类的域很复杂,初始化时占据内存空间很多 这时候建议使用懒汉式即类初始化模式

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×