zoukankan      html  css  js  c++  java
  • 高并发编程基础(锁、线程局部变量)(操作层面)

    常用方法 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是使用时间换空间,是要上锁的,只有一个线程访问完了另外一个线程才能访问,这样子拉长了程序运行时间。

      

      

  • 相关阅读:
    手起刀落-一起来写经典的贪吃蛇游戏
    同步、异步、回调执行顺序之经典闭包setTimeout分析
    起步
    设计模式之单例模式与场景实践
    青春是如此美好,又怎忍平凡度过
    nvm管理不同版本的node和npm
    起步
    基础
    调用wx.request接口时需要注意的几个问题
    微信小程序实现各种特效实例
  • 原文地址:https://www.cnblogs.com/wuzhenzhao/p/9910137.html
Copyright © 2011-2022 走看看