1 引言: MQ运用的场景
(1) 最原始和最传统的时候,两个系统进行交互,一个系统向另一个系统传输数据,假如A系统一次产生10条数据,B数据一次只能处理一条数据,就可能会造memory内存溢出。
然后用消息中间件最中间容器,相当于缓冲,可以想象成一个沙漏。
(2) 在分布式系统中,很多系统,每个系统都有可能会调用其他系统的接口,系统少和数据量小的时候还可以,多的时候就会很复杂,所以用消息中间件,把所有系统产生的数据都放进去,作生产者-消费者模式,然后哪个系统需要调用从中间件里面拿。
2 锁的竞争性
多个线程多把锁:多个线程,每个线程拿到自己的锁,分别获得锁之后执行synchronized方法体的内容。
在方法前面加上关键字synchronized,synchronized取得的锁是对象锁,把对象锁起来了,而不是把一段代码(方法)当成锁,所以哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁,两个对象,线程获得就是两个不同的锁,互不影响。
如果要只设置一把锁,就在synchronized之前加上static,这时候锁的是类,叫独占class类
3 线程安全
线程安全的概念:当多个线程访问一个类(对象或方法)时,该类总是表现出正确的行为,那么这个类就叫做线程安全的。
Synchronized:可以在任意对象及方法上加锁,加锁的这片区域叫“互斥区”或“临界区”。
public class MyThread extends Thread {
privateintcount = 5;
@Override
public synchronizedvoid run() {
count--;
System.out.println(this.currentThread().getName() + ":" +count);
}
publicstaticvoid main(String[] args) {
MyThreadmyThread = new MyThread();
Threadt1 = new Thread(myThread, "t1");
Threadt2 = new Thread(myThread, "t2");
Threadt3 = new Thread(myThread, "t3");
Threadt4 = new Thread(myThread, "t4");
Threadt5 = new Thread(myThread, "t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
从代码可以看出,当不加synchronized时,当多个线程访问mythread里面run方法时需要排队,这里排队不是指代码的执行顺序而是CPU分配资源的时间顺序。
在加上synchronized时,线程要访问synchronized的方法就要获取锁,别的线程就要等待并争夺锁,存在锁竞争的问题。
实现线程安全需要满足两个特性:原子性和可见性
4 脏读
处理:对对象的一个方法加锁的时候,需要考虑业务的一致性,比如set和get方法,同时加锁synchronized同步关键字,保证业务的原子性。
业务场景:A在9:00 从Oracle数据里面selec出一条数据的值,假设原值是100,假设Oracle做full scan 操作,在9:10查到数据值,刚好B在9:05 对数据路中该值做了个DML操作,新值是200,问A取出的值是多少?
100.因为Oracle数据库有个一致性,在9:00查的时候就是查9:00这个表的数据,如果数据有更改过程,Oracle有个undo操作去存放原值,A做select查出的就是这个原值。
5 业务场景
锁的重入,场景:三个方法都加上锁,每个方法依次调用下个方法的值,这是可以的,不会出现异常。
子类调用父类共享方法,也是没问题的
使用锁对异常的处理态度要谨慎,比如执行一个队列任务,第一个对象出现了异常,然后业务逻辑没执行完就释放了锁,那么可想而知后面的对象执行的都是错误的,所以编写代码时候要谨慎,比如捕获到了异常应该怎样做更合适一点。
6 volatile 关键字
使变量在多个线程间可见
publicclass VolatileTest extends Thread {
privatevolatilebooleanflag = true;
publicboolean isFlag() {
returnflag;
}
publicvoid setFlag(booleanflag) {
this.flag = flag;
}
@Override
publicvoid run() {
System.out.println("进入rt线程");
while (flag) {
// 执行循环体
}
System.out.println("rt线程已关闭");
}
publicstaticvoid main(String[] args) throwsInterruptedException {
System.out.println("主线程开始");
VolatileTestvolatileTest = newVolatileTest();
volatileTest.start();
volatileTest.sleep(2000);
volatileTest.setFlag(false);
Thread.sleep(1000);
System.out.println(volatileTest.isFlag());
}
}
总结:在java中,每块线程都有一块工作内存,其中存放着所有线程共享的主内存变量值的拷贝,是为了优化,这样每次启动线程都不用去主线程里面取变量值,线程执行的时候会去操作这些存放在自己工作内存的变量。为了存取一个共享的变量,一个线程通常先获取锁定并去清除它的内存工作区,把这些共享变量从所有的线程的共享内存去中正确的装入到他所在的工作内存区,当线程解锁的时候保证该工作内存区变量的值写回到共享内存。
一个线程可以执行的操作,use,assign,load,store,lock,unlock.
主内存可以执行的操作,read,write,lock,unlock,每个操作都是原子的。
7 volatile和synchronized 的区别
Sysnchronized会影响性能,比如多线程访问一个变量,操作一个变量,因为是加锁的,所以别的线程访问的时候,是访问不了这个变量的,就会造成性能上的问题。Volatile 可以实时操作变量,更改完后将变量写回线程的共享内存中。
但是Volatile具有多个线程之间的可见性,不具备同步性(即原子性),可以算一个轻量级的syschronized,性能比syschronized强很多,不会造成阻塞。(加上static也不能保证原子性)
不具备同步性,比如这个线程更改了变量A的值,那边变量已经操作变量A旧值一段时间了,也就一直用旧值,这边就不能保证同步性了,所谓同步性就是我操作这个变量值的时候,除非我释放锁,不然别的别想动,所以volatile在做数据改写方面不是很好,在仅读的场景下就非常适合了。
总结,volatile只具备多个线程的可见操作,并不能代替synchronized的同步功能。
要想实现原子性,java.util.concurrent.atomic类,去修饰变量。
public class AtomicUse {
public staticAtomicInteger count = new AtomicInteger(0);
public intmultiAdd() { //加锁和不加锁的区别
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
count.addAndGet(1);
count.addAndGet(2);
count.addAndGet(3);
count.addAndGet(4); //+10操作
return count.get();
}
public staticvoid main(String[] args) {
finalAtomicUse atomicUse = new AtomicUse();
List<Thread>ts = new ArrayList<Thread>();
for(inti=0; i<100; i++) {
ts.add(newThread(new Runnable() {
publicvoid run() {
System.out.println(atomicUse.multiAdd());
}
}));
}
for(Threadthread : ts) {
thread.start();
}
}
}
打印出来:
多个addAndGet在一个方法内是非原子性的,需要加上synchronized修饰,保证4个addAndGet整体原子性。上面代码暴露的问题就是,对于一个方法addAndGet(1)或者别的单独的肯定是原子性的,但是多个方法然后多线程去访问就会暴露问题。
总结:atomic类只保证本身方法的原子性,并不保证多个操作的原子性。
实现线程间的通信,将一个个单个线程汇合成一个整体,实现系统间的交互性,提高CPU的使用率。
使用Wait和notify 实现线程间通信。
wait和notify是自Object类的方法,也就是每个类都具有的方法,必须配合syschronized使用。
Wait方法释放锁,notify不释放锁(起到通知的作用)
/*
* 实现效果:两个线程,线程A负责向list添加值,当list.size==5时,抛出异常。
*/
public class ListAdd1 {
privatevolatile List list1 = new ArrayList();
public voidadd() {
list1.add("hello");
}
public intget() {
return list1.size();
}
public staticvoid main(String[] args) {
final ListAdd1 listAdd1 = new ListAdd1();
Thread t1 = new Thread(new Runnable() {
@Override
publicvoid run() {
for(inti = 0; i < 10;i++) {
System.out.println("listAdd1插入了新值:"+Thread.currentThread().getName());
listAdd1.add();
try{
Thread.sleep(500);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
publicvoid run() {
while(true){
if(listAdd1.get()== 5) {
System.out.println("当前线程收到通知:"+Thread.currentThread().getName()+"list.size == 5" );
thrownew RuntimeException();
}
}
}
},"t2");
t1.start();
try {
t1.sleep(5);
} catch(InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
有个弊端每次线程t1执行的时候,线程2都在这边做判断和循环,影响性能。
接下来用wait和notify来实现
public class ListAdd2 {
privatevolatile List list2 = new ArrayList();
public voidadd() {
list2.add("hello");
}
public intget() {
returnlist2.size();
}
public staticvoid main(String[] args) {
finalListAdd2 listAdd2 = new ListAdd2();
// 实例化一个lock,配合synchronized使用
finalObject lock = new Object();
Threadt1 = new Thread(new Runnable() {
@Override
publicvoid run() {
synchronized(lock) {
for(int i = 0; i < 10; i++) {
System.out.println("listAdd1插入了新值:" +Thread.currentThread().getName());
listAdd2.add();
try{
Thread.sleep(500);
}catch (InterruptedException e) {
e.printStackTrace();
}
if(listAdd2.get() == 5) {
System.out.println("已经发出通知了");
lock.notify();
}
}
}
}
},"t1");
Threadt2 = new Thread(new Runnable() {
@Override
publicvoid run() {
synchronized(lock) {
if(listAdd2.get() != 5) {
try{
lock.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("当前线程收到通知:" +Thread.currentThread().getName() + "list.size == 5");
thrownew RuntimeException();
}
}
},"t2");
t2.start();
try {
t1.sleep(5);
}catch (InterruptedException e) {
e.printStackTrace();
}
t1.start();
}
}
注:notify不释放锁,就算wait唤醒锁,也要等nodify那个线程执行完毕后才能获取到锁。
还可以利用Concurrent包下的方法
如将上面的lock换成
final CountDownLatch countDownLatch = newCountDownLatch(1);
去掉synchronized(lock)
lock.notify(); -> countDownLatch.countDown();
lock.wait(); -> countDownLatch.await();
但是CountDownLatch不是基于锁机制的,跟锁没有一毛钱关系,就是判断条件,条件满足时countDownLatch.countDown();发出信号,然后countDownLatch.await();接收信号,执行下面方法。
经典场景,用于远程连接,一个非阻塞的线程,执行某个任务,想让它暂停几秒,阻塞,但是它阻塞的时候,它下面的线程还在一直执行,这时候就用待countDownLatch.
8 使用LinkList和wait、notify方法模拟阻塞Queue
阻塞Queue:从queue里面take(取第一个值)时,如果queue为空的话就让线程等待(阻塞),一直等待,直到有新数据插入为止,向queue里面put(最后面加值)时,如果queue满了,就让线程等待(阻塞),直到有数据被取出为止.
public class SimulateQueue {
privateAtomicInteger count = new AtomicInteger(0); //计数器
final IntegerminSize = 0;
final IntegermaxSize;
publicSimulateQueue(Integer size) {
maxSize = size;
}
private finalLinkedList<Object> list = new LinkedList<Object>();
private finalObject lock = new Object();
public voidput(Object obj) {
synchronized (lock) {
if(count.get()== this.maxSize) {
try{
lock.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(obj);
count.incrementAndGet();
lock.notify();
System.out.println("新插入的元素为:" + obj);
}
}
public Objecttake() {
Object obj = null;
synchronized (lock) {
if(count.get()== this.minSize) {
try{
lock.wait();
}catch (InterruptedException e) {
//TODO Auto-generated catch block
e.printStackTrace();
}
}
obj= list.removeFirst();
count.decrementAndGet();
lock.notify();
System.out.println("取出的元素是:"+obj);
returnobj;
}
}
public IntegergetSize() {
return this.count.get();
}
public staticvoid main(String[] args) {
SimulateQueuequeue = new SimulateQueue(1);
Threadt1 = new Thread(new Runnable() {
@Override
publicvoid run() {
queue.put("h1");
System.out.println("queue里面放入了 h1");
queue.put("h2");
System.out.println("queue里面放入了 h2");
}
},"t1");
t1.start();
Threadt2 = new Thread(new Runnable() {
@Override
publicvoid run() {
Objectobj = null;
obj= queue.take();
System.out.println("取出"+obj);
obj= queue.take();
System.out.println("取出"+obj);
}
},"t2");
t2.start();
}
以下场景引发的问题:
场景一:如果锁lock是一个字符串,然后线程A访问的时候将lock的字符串的值更改了,会发生什么?
答案:锁跟没有了一样,更改了值相当于换了把锁,线程B访问的时候也就是另一把锁了。
场景二:如果锁lock是一个对象,对象的属性改变了,会发生什么?
答案:不影响。
锁的优化:细致化锁,将锁锁在需要加锁的代码行上,性能优化。
9 ThreadLocal创建局部变量
ThreadLocal:线程局部变量,是一种多线程并发访问变量的解决方案。与其synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全.
分析:在性能上,ThreadLocal不具有绝对的优势,在并发不是很高的时候,加锁的性能会更好,但作为一套与锁完全无关的线程解决方案,在高并发或者竞争激烈的场景,使用ThreadLocal可以在一定的程度上减少锁竞争。
示例代码:
public class ThreadLocalTest {
privateThreadLocal<String> th = new ThreadLocal<String>();
public voidset(String value) {
th.set(value);
}
public Stringget() {
System.out.println(Thread.currentThread().getName()+th.get());
return th.get();
}
public staticvoid main(String[] args) {
ThreadLocalTestthreadLocal = new ThreadLocalTest();
Threadt1 = new Thread(new Runnable() {
@Override
publicvoid run() {
threadLocal.set("hello");
threadLocal.get();
}
},"t1");
Threadt2= new Thread(new Runnable() {
@Override
publicvoid run() {
try{
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
threadLocal.get();
}
},"t2");
t1.start();
t2.start();
}
}
控制台打印:
t1hello
t2null
分析:t1设置的局部变量t2获取不到