马士兵老师高并发编程基础入门

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_34993631/article/details/82425052

锁是指谁?

Object o = new Object();

Synchronized(o);

我们一般认为Synchronized锁定的是这段代码块但事实上,Synchronized锁定的是锁这个对象。不仅如此Synchronized锁定的是heap内存中的这个对象而不是这个引用。

一个例子

/**
 * 锁定某对象o,如果o的属性发生改变,不影响锁的使用
 * 但是如果o变成另外一个对象,则锁定的对象发生改变
 * 应该避免将锁定对象的引用变成另外的对象
 * @author mashibing
 */
package yxxy.c_017;

import java.util.concurrent.TimeUnit;


public class T {
	
	Object o = new Object();

	void m() {
		synchronized(o) {
			while(true) {
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName());
				
				
			}
		}
	}
	
	public static void main(String[] args) {
		T t = new T();
		//启动第一个线程
		new Thread(t::m, "t1").start();
		
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//创建第二个线程
		Thread t2 = new Thread(t::m, "t2");
		
		t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
		
		t2.start();
		
	}

Java的锁的本质就是内存对象上的一段信息,刚开始t2线程是抢不到那一把锁的因为被t1所占。但是后来o指向了另一个全新的对象这个在堆内存中的对象还没有被当做锁使用所以t2就拿它来当做自己的锁。见下图:

拓展:

由于锁的特性所以一般我们不要使用字符串常量来作为锁对象,这样会使得线程莫名的阻塞。看起来是两个字符串的引用但是他们指向的是同一段的内存。

重入锁

在同一个线程中同步代码块可以多次获得同一把锁。这种情况叫做可重入锁。当然还有一些引用计数的规则等等。这里重点强调的是调用,一旦是调用那么也就说明他们两个是在同一个方法中的。

Synchronized(m){

Synchronized(m){

   

}

}

ReentrantLock的特性

·这是一种重入锁的实现。它有一个很大的特点就是必须的手动开启锁和释放锁。尤其是这个释放锁,不能忘记否则程序则会一直阻塞。与Synchronized不同的是Synchronized在遇到异常的时候就会释放锁但是ReentrantLock在异常下是不会释放锁的,因此经常在finally中进行锁的释放。

·locked = lock.tryLock(5, TimeUnit.SECONDS);尝试去获得锁,如果5秒还是没有获得到那么就会向下是执行。

·可以被打断的锁,如果一段代码被lock.lockInterruptibly(); 这个锁锁住,那么他能够被

t2.interrupt();这样的语句去手动打断。

·公平锁与不公平锁,通常来说Synchronized的锁是一种不公平的锁,而ReentrantLock可以实现公平锁。那什么是不公平呢?就是由于随机化的原因有的线程会由于运气不好久久得不到执行。公平就是使用了一种时间上的调度算法来使得每个线程的都能够得到公平的执行。

线程通信的底层


线程通信通常有两种,一种是读取共享的一段内存,还有一种就是线程之间互相通信。Java的线程通讯采用的是读取共享的一段内存。

Volatile关键字

这是一个案例,在上述代码不开volatile的时候new出来的那个线程把running从主内存中读取出来读到自己的缓冲区中并且以此为标签来执行while()中的代码块。而且一直执行下去在繁忙的情况下不会去读取主内存中的值,即使main线程对这个值做了修改。所以我们会看到while()中的代码一直被执行。

当volatile开启的时候,这两个线程之间也就变成了可见的了。具体原因就是在主内存的running的值被修改之后这时有一个线程会通知new出来的线程但缓冲区说你的running的值已经过期了,所以缓冲区的running的值会变成false随之while的执行结束。

值得一提的是在线程空闲的时候有可能会去主内存中读取值。

拓展:Volatile与Synchronized的联系与区别

Volatile只保证可见性,就是线程之间的变量是互相可见的,但是不能够保证原子性。

Synchronized,同时保证了原子性和可见性,因为Synchronized会将程序串行执行,当上一个程序执行完毕之后会将值写入到主内存中下一个线程来到时肯定会读取到这段内容。

这种感觉类似于数据库中的事务隔离级别。

AtomicXXX类型

因为++操作(当然也包含其他的系列操作)不是一个原子操作,所以在需要保证原子的++时可以通过上锁来解决问题,也可以通过以下的案例来解决:

/**
 * 解决同样的问题的更高效的方法,使用AtomXXX类
 * AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
 * @author mashibing
 */
package yxxy.c_015;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;


public class T {
	/*volatile*/ //int count = 0;
	
	AtomicInteger count = new AtomicInteger(0); 

	/*synchronized*/ void m() { 
		for (int i = 0; i < 10000; i++)
			//if count.get() < 1000
			count.incrementAndGet(); //count++
	}

	public static void main(String[] args) {
		T t = new T();

		List<Thread> threads = new ArrayList<Thread>();

		for (int i = 0; i < 10; i++) {
			threads.add(new Thread(t::m, "thread-" + i));
		}

		threads.forEach((o) -> o.start());

		threads.forEach((o) -> {
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});

		System.out.println(t.count);

	}

}

一道淘宝面试题的演化(Volatile与门闩机制)

实现一个容器,提供两个方法,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("t2 结束");
		}, "t2").start();
	}
}

方案二:

在方案一的基础上加上volatile关键字,成功运行。因为两个线程之间是彼此可见的。

volatile List lists = new ArrayList();

方案三:

由于方案二的while的循环一直在监视所以十分的浪费CPU。因此我们将机制改为了wait()与notify()。也就是当t1检测到到达size到达5的时候叫醒正在沉睡的t2。但是t2、t1使用的是通一把锁而notify不会释放锁(当然它与notify起作用的前提是获得锁)。所以我们的最终方案是当t1叫醒t2的同时自己要wait()这样才保证了t2能够拿到锁然后t2执行完之后也要唤醒t1(由于这时候t2已经执行完毕了所以锁自然释放)。代码如下:

public class MyContainer4 {

	//添加volatile,使t2能够得到通知
	volatile List lists = new ArrayList();

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

	public int size() {
		return lists.size();
	}
	
	public static void main(String[] args) {
		MyContainer4 c = new MyContainer4();
		
		final Object lock = new Object();
		
		new Thread(() -> {
			synchronized(lock) {
				System.out.println("t2启动");
				if(c.size() != 5) {
					try {
						lock.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println("t2 结束");
				//通知t1继续执行
				lock.notify();
			}
			
		}, "t2").start();
		
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}

		new Thread(() -> {
			System.out.println("t1启动");
			synchronized(lock) {
				for(int i=0; i<10; i++) {
					c.add(new Object());
					System.out.println("add " + i);
					
					if(c.size() == 5) {
						lock.notify();
						//释放锁,让t2得以执行
						try {
							lock.wait();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
					
					try {
						TimeUnit.SECONDS.sleep(1);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}, "t1").start();
		
		
	}
}

终极方案:

使用门闩机制

使用Latch(门闩)替代wait notify来进行通知

好处是通信方式简单,同时也可以指定等待时间

使用await和countdown方法替代wait和notify

CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行

当不涉及同步,只是涉及线程通信的时候,用synchronized + wait/notify就显得太重了

这时应该考虑countdownlatch/cyclicbarrier/semaphore

@author mashibing

首先new一个门闩CountDownLatch latch = new CountDownLatch(1);里面有一个参数,同时他有一个方法就是latch.await();,它会插在代码之间阻拦代码的执行。同时latch.countDown();方法会减少门闩的数量当门闩的数量减少为0的时候这时门会自动打开这时候latch.await();会向下执行。整个过程不涉及锁的机制,高效得实现了线程之间的通信。

public class MyContainer5 {

	// 添加volatile,使t2能够得到通知
	volatile 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("t2启动");
			if (c.size() != 5) {
				try {
					latch.await();
					
					//也可以指定等待时间
					//latch.await(5000, TimeUnit.MILLISECONDS);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println("t2 结束");

		}, "t2").start();

		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}

		new Thread(() -> {
			System.out.println("t1启动");
			for (int i = 0; i < 10; i++) {
				c.add(new Object());
				System.out.println("add " + i);

				if (c.size() == 5) {
					// 打开门闩,让t2得以执行
					latch.countDown();
				}

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

		}, "t1").start();

	}
}

最后感谢马老师,一个专心做教育的老师。

猜你喜欢

转载自blog.csdn.net/qq_34993631/article/details/82425052