高并发编程基础(锁、线程局部变量)(操作层面)

常用方法 wait() 、notify()、notifyAll():

  通过一个简单的例子来熟悉wait() 、notify()、notifyAll().有一个例子实现一个容器,提供两个方法 add  size,写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到达5的时候,线程2给出提示并结束。我们可以先把大致的程序代码给出:

public class MyContainer1 {
	
   List lists=new ArrayList();
	
	public void add(Object o) {
		lists.add(o);
	}
	public int size() {
		return lists.size();
	}
	
	public static void main(String[] args) {
		MyContainer1 c =new MyContainer1();
		
		new Thread(()-> {
			for(int i=0;i<10;i++) {
				c.add(new Object());
				System.out.println("add"+i);
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		},"t1").start();
		new Thread(()-> {
			while(true) {
				if(c.size()==5) {
					break;
				}
			}
			System.out.println("线程2结束");
		},"t2").start();
	}
}

  通过运行上述代码得到的结果很明显不能达到我们多期望的。我们可以想到 有可能是size到达5的时候 线程t2并不知晓,我们可以在List lists=new ArrayList();前加 volatile 进行尝试,加完我们发现程序运行时OK的,但是这样子任然存在两个问题,第一,由于我们没有使用锁,这个时候如果有另外一个线程也来添加元素,那么集合长度变成6了,可第一个线程却以为时5,第二,由于线程2使用while死循环来监控集合长度变化,非常浪费CPU。我们可以进行进一步的优化。我们可以使用 wait 跟notify 来实现,使用wait跟notify必须进行加锁,需要注意的是wait会释放锁,而notify不会释放锁,代码如下:

public class MyContainer3 {

	List lists = new ArrayList();

	public void add(Object o) {
		lists.add(o);
	}
	public int size() {
		return lists.size();
	}
	public static void main(String[] args) {
		MyContainer3 c = new MyContainer3();
		final Object lock = new Object();

		new Thread(() -> {
			synchronized (lock) {
				System.out.println("线程2开始");

				if (c.size() != 5) {
					try {
						lock.wait(); //释放锁,进入等待状态
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println("线程2结束");
			}
		}, "t2").start();

		new Thread(() -> {
			System.out.println("线程1开始");
			synchronized (lock) {
				for (int i = 0; i < 10; i++) {
					c.add(new Object());
					System.out.println("add" + i);
					if (c.size() == 5) {
						lock.notifyAll();//唤醒该锁定对象的等待的线程  但是不会释放锁 sleep也不释放锁
					}
					try {
						TimeUnit.SECONDS.sleep(1);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println("线程1结束");
			}
		}, "t1").start();
	}
}

  运行上述小程序,会发现当size等于5的时候,t2线程并没有立即结束,等到了t1运行完以后才结束,这是因为notify并不释放锁,虽然把t2线程叫醒了  ,可是此刻锁再t1线程的受伤,必须等到t1结束,我们进一步优化,当t1执行完notify之后 调用wait 使自己进入等待释放锁,然后t2运行,运行结束再调用notify 唤醒t1.这样才能得到题目一致的效果。由于本程序中使用了synchronized锁,所以其性能可能会有一定的降低,在这里我们可以通过其他手段来实现,并且能保证较好的性能嘛? 我们可以使用 由于此处不涉及同步,仅仅涉及线程之间的同步,synchronized就显得有点笨重,所以我们可以考虑使用 门闩 CountDownLatch来实现。CountDownLatch 的await 跟countDown方法来代替wait跟notify,CountDownLatch不涉及锁,在count等于0的时候程序继续运行,通讯简单,同时也可以指定时间。来看以下代码:

public class MyContainer5 {

	//
	List lists = new ArrayList();

	public void add(Object o) {
		lists.add(o);
	}

	public int size() {
		return lists.size();
	}

	public static void main(String[] args) {
		MyContainer5 c = new MyContainer5();
		CountDownLatch latch =new CountDownLatch(1);
		new Thread(() -> {
				System.out.println("线程2结束");

				if (c.size() != 5) {
					try {
						latch.await();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println("线程2结束");
		}, "t2").start();

		new Thread(() -> {
			System.out.println("线程1开始");
				for (int i = 0; i < 10; i++) {
					c.add(new Object());
					System.out.println("add" + i);
					if (c.size() == 5) {
						latch.countDown();
					}
					try {
						TimeUnit.SECONDS.sleep(1);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println("线程1结束");
		}, "t1").start();

	}

}

  CountDownLatch的概念:

  CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。
CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成了任务,然后在CountDownLatch上等待的线程就可以恢复执行任务。
 

  CountDownLatch的用法:

  CountDownLatch典型用法1:某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
CountDownLatch典型用法2:实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。
 

  CountDownLatch的不足

  CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
 

手动锁 ReentrantLock(重入):

  先上一段小程序代码:

public class ReentrantLock1 {
	
	synchronized void m1() {
		for(int i=0;i<10;i++) {
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.err.println(i);
		}
	}
	synchronized void m2() {
		
			System.err.println("m2.....");
	}
	
	public static void main(String[] args) {
		ReentrantLock1 r1 =new ReentrantLock1();
		new Thread(r1::m1).start();
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		new Thread(r1::m2).start();
	}
}

  上述代码很简单,起两个线程,分别执行两个同步方法,用synchronized锁定this对象,这里只有当m1方法执行完,m2方法才得以运行。在这里我们可以使用手动锁 ReentrantLock 实现同样的功能,直接上代码:

public class ReentrantLock2 {
	Lock lock = new ReentrantLock();
	void m1() {
		try {
			lock.lock();//synchrnized(this) 锁定
			for (int i = 0; i < 10; i++) {

				TimeUnit.SECONDS.sleep(1);
				System.err.println(i);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();//释放锁
		}
	}

	void m2() {
		lock.lock();
		System.err.println("m2.....");
		lock.unlock();
	}

	public static void main(String[] args) {
		ReentrantLock2 r1 = new ReentrantLock2();
		new Thread(r1::m1).start();
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		new Thread(r1::m2).start();
	}
}

  这里使用了 ReentrantLock 代替了 synchronized ,其中锁定采用的 lock() 方法,这里需要注意的是,它不会自动释放锁,也不像 synchronized 那样,在异常发生时jvm会自动释放锁,ReentrantLock 必须要必须要必须要手动释放锁,因此 ReentrantLock的释放锁通常会放到 finally 中去进行锁的释放。通过运行上述小程序会发现它可以达到用synchronized同样的效果。

  在使用ReentrantLock  可以进行“尝试锁定” tryLock 这样无法锁定或者在指定时间内无法锁定,线程可以决定是否继续等待。进行“尝试锁定” tryLock 不管锁定与否,程序都会继续运行,也可以根据trylock的返回值来判断是否运行,也可以指定时间  由于trylock(time)抛出异常 所以unlock一定要在finally里面执行。直接上代码:

public class ReentrantLock3 {
	
	Lock lock = new ReentrantLock();
	void m1() {
		try {
			lock.lock();//synchrnized(this)
			for (int i = 0; i < 10; i++) {
				TimeUnit.SECONDS.sleep(1);
				System.err.println(i);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	/**
	 * 进行“尝试锁定” tryLock 不管锁定与否,程序都会继续运行
	 * 也可以根据trylock的返回值来判断是否运行
	 * 也可以指定时间  由于trylock(time)抛出异常 所以unlock一定要在finally里面执行
	 */
	void m2() {
		boolean locked = lock.tryLock();//拿到锁返回true
		System.err.println("m2....." + locked);
		if(locked) lock.unlock();
	}

	public static void main(String[] args) {
		ReentrantLock3 r1 = new ReentrantLock3();
		new Thread(r1::m1).start();
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		new Thread(r1::m2).start();
	}
}

  运行小程序发现m2方法执行后并未得到锁。这里由于是demo程序,m2方法中的逻辑并不完善,需要根据自己的业务需求来根据locked的值进行处理。这里还可以进行另外一种尝试锁定,修改m2方法如下:

void m2() {
	boolean locked =false;
	try {
		lock.tryLock(5, TimeUnit.SECONDS);
		System.err.println("m2....." +  locked);
	} catch (InterruptedException e) {

		e.printStackTrace();
	}finally {
		if(locked) lock.unlock();
	}
}

  上述m2方法中的尝试锁定lock.tryLock(5, TimeUnit.SECONDS); 的意思是进行5秒钟的尝试锁定,当然这里跟上一步的没有参数的尝试锁定也是一样的,需要根据返回值进行下一步的业务逻辑。

  ReentrantLock 还可以调用lockInterruptibly方法。可以对线程interrupt方法做出相应,在一个线程等待锁的过程中 可以被打断,来看以下代码:

public class ReentrantLock4 {
	static boolean b =false;
	public static void main(String[] args) {
		Lock lock = new ReentrantLock();
		Thread t1 = new Thread(()-> {
			try {
				lock.lock();//synchrnized(this)
				System.err.println("t1  start");
					TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
					System.err.println("t1  end");
			} catch (InterruptedException e) {
				System.err.println("interrupt");
			} finally {
				lock.unlock();
			}
		});
		t1.start();
		Thread t2 = new Thread(()-> {
			try {
//				lock.lock();
				lock.lockInterruptibly();//可以对线程interrupt方法做出相应
				 b = lock.tryLock();
				System.err.println("t2  start");
				TimeUnit.SECONDS.sleep(5);
				System.err.println("t2  end");
			} catch (InterruptedException e) {
				System.err.println("interrupt");
			} finally {
				if(b)lock.unlock();
			}
		});
		t2.start();
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		t2.interrupt();//打断线程的等待
	}
}

  上述代码中的t1线程sleep的时间是Integer的最大值,我们且当成它睡死了,也就是说它一直占用着这个锁不释放,此时t2线程也想锁定lock,但是一直无法得到这个锁,t2线程就会一直处于等待状态,但是我们现在不想让他等待了,想让t2终止,如果此时t2调用的是 lock.lock(); 方法,那么主线程中的 t2.interrupt(); 是起不了效果的 ,因为 ReentrantLock 的lock 方法没有对 interrupt 的支持。 所以我们会发现线程一直处于运行状态,我们将 lock 方法  替换成 lockInterruptibly,再运行程序会发现 t2 线程被终止了 。

  ReentrantLock 可以指定为公平锁,synchronized 的锁全部都是不公平锁,假设多个线程同时在等待同一把锁,在原来的锁拥有者释放该锁的时候,接下去由谁来获得这把锁是不一定的,要看线程调度器去选择哪个了,也称竞争锁,公平锁就是接下去谁获得锁是由规律的,就是等待线程时间长的获得锁,就像排队买票时一样的道理。直接上代码:

public class ReentrantLock5 extends Thread{
	
	private static ReentrantLock lock = new ReentrantLock(true);//传true表示公平锁
	public  void run() {
		
		for(int i=0;i<100;i++) {
			lock.lock();
			try {
			System.out.println(Thread.currentThread().getName()+"获得锁");
			}finally {
				lock.unlock();
			}
		}
	}
	public static void main(String[] args) {
		ReentrantLock5 r1 =new ReentrantLock5();
		Thread thread = new Thread(r1);
		Thread thread2 = new Thread(r1);
		thread.start();
		thread2.start();
	}
}

  上述代码 new ReentrantLock(true) ;中设定参数 true 表示该锁是一个公平锁,公平锁效率低,但是是公平的,运行上述小程序,控制台打印出的结果是每个线程都是循环去执行,一人执行一次,很公平。

  下看来看一下非常经典的生产者消费者模型:写一个固定容量同步容器 有put get方法以及getCount方法 能够支持2个生产线程跟10个消费线程阻塞调用,我们先使用wait/notify来实现,代码如下:

public class MyContainer1<T> {
	private volatile LinkedList<T> lists = new LinkedList<T>();
	final private int MAX = 10;// 最多10个元素
	private volatile int count = 0;

	public synchronized void put(T t) {
		try {
			TimeUnit.MILLISECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.err.println(Thread.currentThread().getName() + " 获得锁 ");
		while (lists.size() == MAX) {// 为什么用while? 因为在唤醒后 while会在执行一遍才执行wait下面的代码
			try {
				System.err.println(Thread.currentThread().getName() + " 进入等待 ");
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		lists.add(t);
		count++;
		System.out.println("存储值" + t + "当前个数:" + count);
		this.notifyAll();// 通知消费者进行消费 要用notifyAll 要使用notify 有可能叫醒一个生产者

	}

	public synchronized T get() {
		try {
			TimeUnit.MILLISECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		T t = null;
		System.err.println(Thread.currentThread().getName() + " 获得锁 ");
		while (lists.size() == 0) {
			try {
				System.err.println(Thread.currentThread().getName() + " 进入等待 ");
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		t = lists.removeFirst();
		count--;
		System.err.println(Thread.currentThread().getName() + "取到的值:" + t + "" + "当前个数:" + count);
		this.notifyAll();// 通知生产者生产
		return t;
	}

	public static void main(String[] args) {
		MyContainer1<String> c = new MyContainer1<>();
		for (int i = 0; i < 4; i++) {
			new Thread(() -> {
				for (int j = 0; j < 5; j++) {
					c.get();
				}
			}, "c-" + i).start();
		}

		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		for (int i = 0; i < 4; i++) {
			new Thread(() -> {
				for (int j = 0; j < 25; j++) {
					c.put(Thread.currentThread().getName() + "****** " + j);
				}
			}, "p-" + i).start();
		}
	}
}

  上述代码是很经典的生产者消费者模型,在这个模型中为什么用while 而不是 if ?这是因为当 notifyAll 唤醒线程的时候要准备往集合中生产,这个时候如果使用的是 if 那么当此刻有两个生产者者线程被唤醒了,而集合中已经有9个元素,此刻只要有一个线程存进去一个值,另外一个线程一定报错,如果用的是 while 。那么在被唤醒的时候 线程会继续执行 lists.size() == MAX 这段代码进行判断。这就是为什么要使用while 而不是 if 的原因。还有一点是为什么使用notifyAll 而不是 notify ? 这是因为前者能唤醒所有等待的线程,而后者只能随机唤醒一个,假设此刻运行的生产者线程 put 了一个值后 ,集合数量达到10,而此刻是用 notify 的话,恰好又是唤醒一个生产者,此刻这个线程发现集合满了,也进入 wait 状态,导致程序无法运行。在这里我们会发现使用notifyAll的时候唤醒所有线程,当生产者往集合里 put 满了元素后还有可能继续唤醒生产者,是否能做到精准的就叫醒消费者线程呢?

  ReentrantLock 中可以使用 lock 跟Condition来实现, Condition可以更加精准的指定哪些线程被唤醒。来看以下代码:

public class MyContainer2<T> {

	final private LinkedList<T> lists = new LinkedList<T>();
	final private int MAX = 10;// 最多10个元素
	private static int count = 0;

	private Lock lock = new ReentrantLock();
	private Condition p = lock.newCondition(); //生产者
	private Condition c = lock.newCondition(); //消费者

	public void put(T t) {
		try {
			lock.lock();
			while (lists.size() == MAX) {
				p.await();
			}
			lists.add(t);
			++count;
			System.out.println("存储值" + t + "当前个数:" + count);
			c.signalAll();
		} catch (InterruptedException e) {

		} finally {
			lock.unlock();
		}
	}
	public T get() {
		T t = null;
		try {
			lock.lock();
			while (lists.size() == 0) {
				c.await();
			}
			t = lists.removeFirst();
			count--;
			System.err.println(Thread.currentThread().getName() + "取到的值:" + t + "" + "当前个数:" + count);
			p.signalAll();
		} catch (InterruptedException e) {

		} finally {
			lock.unlock();
		}
		return t;
	}

	public static void main(String[] args) {
		MyContainer2<String> c = new MyContainer2<>();
		for (int i = 0; i < 10; i++) {
			new Thread(() -> {
				for (int j = 0; j < 5; j++) {
					c.get();
				}
			}, "c" + i).start();
		}
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		for (int i = 0; i < 2; i++) {
			System.out.println("生产者" + i + "启动");
			new Thread(() -> {
				for (int j = 0; j < 25; j++) {
					c.put(Thread.currentThread().getName() + "****** " + j);
				}
			}, "p" + i).start();
		}
	}
}

  

ThreadLocal (线程局部变量):

  ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。我们先来看以下代码:

public class ThreadLocal1 {
	volatile static Person p=new Person();
	
	public static void main(String[] args) {
		new Thread(()-> {
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(p.name);
		}).start();
		
		new Thread(()-> {
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			p.name = "lisi";
		}).start();
	}
}
class Person{
	String name ="zhangsan";
}

  上述小程序中两个线程之间是相互影响的,线程2修改了name ,线程一拿到的结果是 lisi 。如果我们要实现两个线程之间互不影响有什么好的方法呢? 这里我们可以使用 ThreadLocal 线程局部变量来实现:

public class ThreadLocal1 {
	
	 static ThreadLocal<Person> tl=new ThreadLocal<>();
	
	public static void main(String[] args) {
		new Thread(()-> {
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(tl.get());
		}).start();
		
		
		new Thread(()-> {
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			tl.set(new Person());
		}).start();
	}
}
class Person{
	String name ="zhangsan";
}

  运行上述小程序得到输出结果为 null ,这样就保证了线程2 里的 Person 对象与线程1是互不影响的。也就是自己只能用自己线程里的东西。因为ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。其他线程是没办法访问的 。ThreadLocal是使用空间换时间,无需上锁,提高了并发效率,就像Hibernate的session就存在于TreadLocal中,都由线程自己维护,这样子就不存在线程之间的等待问题。synchrnized是使用时间换空间,是要上锁的,只有一个线程访问完了另外一个线程才能访问,这样子拉长了程序运行时间。

 

  

  

 

posted @ 2018-11-05 17:04  吴振照  阅读(718)  评论(0编辑  收藏  举报