• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • YouClaw
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
在赶路的我
博客园    首页    新随笔    联系   管理    订阅  订阅

JUC并发编程

JUC并发编程基础(java.util.concurrent)

  • 面试:单例模式,排序算法,生产者消费者模式,死锁

传统的Synchronized

package demo1;

import java.util.Iterator;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test {

	public static void main(String[] args) {

		Tickets tickets = new Tickets();

		new Thread(() -> {
			while (true) {
				tickets.sendTicket();
			}

		}, "A").start();
		new Thread(() -> {
			while (true) {
				tickets.sendTicket();
			}
		}, "B").start();
		new Thread(() -> {
			while (true) {
				tickets.sendTicket();
			}
		}, "C").start();

	}

}

class Tickets {

	private int number = 20;

	public synchronized void sendTicket() {
		if (number > 0) {
			System.out.println(Thread.currentThread().getName() + "卖出了第" + number-- + "票,还剩" + number + "张票");
		} else {
			return;
		}

	}

}

线程通信

  • 使用this.wait()和this.notify(),this.notifyAll()来进行线程之间的通信
package demo1;

public class Test2 {

	public static void main(String[] args) {

		Ticketss t = new Ticketss();

		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.sendNumber();
			}
		}, "A1").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.sendNumber();
			}
		}, "A2").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.sendNumber();
			}
		}, "A3").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.sendNumber();
			}
		}, "A4").start();

		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.getNumber();
			}
		}, "B1").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.getNumber();
			}
		}, "B2").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.getNumber();
			}
		}, "B3").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.getNumber();
			}
		}, "B4").start();

	}

}

class Ticketss {

	private int number = 0;
	// 生产者
	public synchronized void sendNumber() {

		while (number != 0) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		number++;
		System.out.println(Thread.currentThread().getName()+"生产了第"+number+"张票");
		this.notifyAll();

		
	}

	// 消费者
	public synchronized void getNumber() {

		while (number == 0) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		number--;
		System.out.println(Thread.currentThread().getName()+"消费了第"+number+"张票");
		this.notifyAll();
	}

}
  • synchronized和this.wait(),this.notify(),this.notifyAll()配合来使用实现线程之间的通信
    • this.wait()表示当前使用synchronized修饰的同步方法或者代码块在当前锁监视器中等待
    • this.notify()表示唤醒当前锁监视器中正在等待的所有线程中的其中一个线程
    • this.notifyAll()表示唤醒当前锁监视器中正在等待的所有线程
    • 需要注意的是理解这部分内容需要清楚synchronized方法或者代码块的锁是谁

Lock锁(更加灵活)

  • Lock分为公平锁和非公平锁
  • 公平锁:不管任何原因,每一个线程都必须按照顺序来执行
  • 非公平锁:允许插队,优先执行执行时间少的线程
package demo1;

import java.util.Iterator;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test {

	public static void main(String[] args) {

		Tickets tickets = new Tickets();

		new Thread(() -> {
			tickets.sendTicket();
		}, "A").start();
		new Thread(() -> {
			tickets.sendTicket();
		}, "B").start();
		new Thread(() -> {
			tickets.sendTicket();
		}, "C").start();

	}

}

class Tickets {

	private int number = 20;
	
	Lock lock = new ReentrantLock();

	public  void  sendTicket() {
		lock.lock();
		try {
			
			while (true) {

				if (number > 0) {
					System.out.println(Thread.currentThread().getName() + "卖出了第" + number-- + "票,还剩" + number + "张票");
				}else {
					break;
				}

			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally {
			lock.unlock();
		}
	}

}

Lock锁实现线程之间的通信

package demo1;

import java.util.Iterator;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test {

	public static void main(String[] args) {

		Tickets tickets = new Tickets();
		// 生产者线程
		new Thread(() -> {
			for (int i = 0; i < 100; i++) {
				
				tickets.sendNumber();
		
			}

		}, "A1").start();
		new Thread(() -> {
			for (int i = 0; i < 100; i++)

				tickets.sendNumber();

		}, "A2").start();
		new Thread(() -> {
			for (int i = 0; i < 100; i++)

				tickets.sendNumber();

		}, "A3").start();
		new Thread(() -> {
			for (int i = 0; i < 100; i++)

				tickets.sendNumber();

		}, "A4").start();
		// 消费者线程
		new Thread(() -> {
			for (int i = 0; i < 100; i++)

				tickets.getNumber();

		}, "B1").start();
		new Thread(() -> {
			for (int i = 0; i < 100; i++) {
				tickets.getNumber();
			}

		}, "B2").start();
		new Thread(() -> {
			for (int i = 0; i < 100; i++) {
				tickets.getNumber();
			}

		}, "B3").start();
		new Thread(() -> {
			for (int i = 0; i < 100; i++) {
				tickets.getNumber();
			}

		}, "B4").start();

	}

}

class Tickets {

	private int number = 0;

	Lock lock = new ReentrantLock();

	Condition condition = lock.newCondition();

	// 生产者
	public void sendNumber() {

		lock.lock();
		try {
		
			if (number == 0) {
				number++;
				System.out.println(Thread.currentThread().getName() + "生产了" + number + "张票");
				condition.signalAll();
			} else {
				
				condition.await();
			}

		} catch (Exception e) {
			e.getMessage();
		} finally {

			lock.unlock();
		}
	}

	// 消费者
	public void getNumber() {

		lock.lock();

		try {


			if (number != 0) {
				System.out.println(Thread.currentThread().getName() + "消费了" + number + "张票");
				number--;
			} else {
				condition.signalAll();

				condition.await();
			}

		} catch (Exception e) {
			e.getMessage();
		} finally {

			lock.unlock();
		}
	}

}

  • Lock锁对象配合Condition锁监视器对象来完成线程之间的通信,而且最重要的是可以实现精准唤醒
    • 一个Lock锁对象可以创建多个Condition锁监视器,实现精准唤醒
    • condition.await()让当前线程在condition锁监视器中等待
    • condition.single()让在condition锁监视器中所有等在的线程唤醒其中一个
    • condition.singleAll()让在condition锁监视器中所有等待的线程全部唤醒

传统的Synchronized锁和Lock锁的区别

区别

  1. 来源:lock是一个接口,而synchronized是java的一个关键字,是内置语言实现
  2. 异常是否释放锁:synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
  3. 是否响应中断:lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断
  4. 性能:Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择
  5. 线程调度(通信):synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度。

用法区别

  1. synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象
  2. lock:一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁

性能区别

  • synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。
  • 在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。
  • 但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。

采用cup锁机制不同

  • synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。 独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
  • 而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。 我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
  • 现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

用途区别

  • synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。
    1. 某个线程在等待一个锁的控制权的这段时间需要中断
    2. 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
    3. 具有公平锁功能,每个到来的线程都将排队等候
  • 先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B 2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制:可中断/可不中断
    1. B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此)
    2. B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃

传统的生产者消费者问题(防止虚假唤醒)

  • 在线程中出现if判断是可以造成虚假唤醒,导致线程出现问题
  • if判断只会判断一次,没法观察其他线程对同一资源的实时改变
package demo1;

public class Test2 {

	public static void main(String[] args) {
//业务是所有的生产者总共生产1个数,所有消费者中只有一个能够消费,由于代码不规范出现虚假唤醒,导致所有的生产者同时唤醒后生产出多个
		Ticketss t = new Ticketss();

		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.sendNumber();
			}
		}, "A1").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.sendNumber();
			}
		}, "A2").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.sendNumber();
			}
		}, "A3").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.sendNumber();
			}
		}, "A4").start();

		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.getNumber();
			}
		}, "B1").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.getNumber();
			}
		}, "B2").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.getNumber();
			}
		}, "B3").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.getNumber();
			}
		}, "B4").start();

	}

}

class Ticketss {

	private int number = 0;

	// 生产者
	public synchronized void sendNumber() {

        
		if (number != 0) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		number++;
		System.out.println(Thread.currentThread().getName()+"生产了第"+number+"张票");
		this.notifyAll();

		
	}

	// 消费者
	public synchronized void getNumber() {

		if (number == 0) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		number--;
		System.out.println(Thread.currentThread().getName()+"消费了第"+number+"张票");
		this.notifyAll();

		

	}

}
  • 解决方法:将if改为while
package demo1;

public class Test2 {

	public static void main(String[] args) {

		Ticketss t = new Ticketss();

		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.sendNumber();
			}
		}, "A1").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.sendNumber();
			}
		}, "A2").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.sendNumber();
			}
		}, "A3").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.sendNumber();
			}
		}, "A4").start();

		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.getNumber();
			}
		}, "B1").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.getNumber();
			}
		}, "B2").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.getNumber();
			}
		}, "B3").start();
		new Thread(() -> {
			for (int i = 0; i < 20; i++) {
				t.getNumber();
			}
		}, "B4").start();

	}

}

class Ticketss {

	private int number = 0;

	// 生产者
	public synchronized void sendNumber() {

		while (number != 0) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		number++;
		System.out.println(Thread.currentThread().getName()+"生产了第"+number+"张票");
		this.notifyAll();

		
	}

	// 消费者
	public synchronized void getNumber() {

		while (number == 0) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		number--;
		System.out.println(Thread.currentThread().getName()+"消费了第"+number+"张票");
		this.notifyAll();

		

	}

}
  • 虚假唤醒出现的原因是没有正确有效的判断导致的,导致多个线程不能实时观察同一资源的变化情况
  • 解决方法:
    • 使用while来持续判断唤醒的条件
    • 改变原来的代码逻辑,让线程唤醒后,还能实时知晓同一资源的情况

彻底理解synchronized的锁到底是谁

结论

  • 如果synchronized同步方法或者代码块没有使用static来修饰,就是普通的synchronized,那么他的锁就是调用它的对象
  • 如果使用了static来修饰,他的锁调用对象的类

例子

package demo1;

import java.util.concurrent.TimeUnit;

public class Test3 {
	
	public static void main(String[] args) {
		
		
		Message message = new Message();
		Message message2 = new Message();
		
		new Thread(()->{message.sendMsg();},"A").start();
		new Thread(()->{message.call();},"B").start();
		
	}

}


class Message{
	public synchronized void sendMsg() {
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("发短信");
		
	}
	
	public synchronized void call() {
		System.out.println("打电话");
	}
}
  • 在这里使用message对象来调用sendMsg和call同步方法,所以两个同步方法用的是同一把锁,发短信先拿到锁,所以先执行发短信,释放锁后,再执行打电话
package demo1;

import java.util.concurrent.TimeUnit;

public class Test3 {
	
	public static void main(String[] args) {
		
		
		Message message = new Message();
		Message message2 = new Message();
		
		new Thread(()->{message.sendMsg();},"A").start();
		new Thread(()->{message2.call();},"B").start();
		
	}

}


class Message{
	public synchronized void sendMsg() {
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("发短信");
		
	}
	
	public synchronized void call() {
		System.out.println("打电话");
	}
}
  • 在这里分别创建了message和message2对象来发短信和打电话,所以发短信的锁是message对象,打电话的锁是message2对象,所以先打电话在发短信
package demo1;

import java.util.concurrent.TimeUnit;

public class Test3 {
	
	public static void main(String[] args) {
		
		
		Message message = new Message();
		Message message2 = new Message();
		
		new Thread(()->{message.sendMsg();},"A").start();
		new Thread(()->{message2.call();},"B").start();
		
	}

}
class Message{
	public static synchronized void sendMsg() {
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("发短信");
		
	}
	
	public static synchronized void call() {
		System.out.println("打电话");
	}
	
}
  • 在这里使用static来修饰同步方法,也就是发短信和打电话两个同步方法的锁是Message类,即使使用不同的对象来分别调用发短信和打电话也是使用的同一把锁,所以先发短信,再打电话

高并发下的集合(不安全的)

Collection

传统的List集合在高并发下是不安全的

package demo1;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class Arr {
	public static void main(String[] args) {
		List<String> strList = new ArrayList<>();
		
		//多个线程同时操作list集合
		new Thread(()->{
			for(int i=0;i<10;i++) {
				strList.add(UUID.randomUUID().toString().substring(0,4));
				System.out.println(Thread.currentThread().getName()+"=>"+strList);
			}
		},"A").start();
		
		new Thread(()->{
			for(int i=0;i<10;i++) {
				strList.add(UUID.randomUUID().toString().substring(0,4));
				System.out.println(Thread.currentThread().getName()+"=>"+strList);
			}
		},"B").start();
		
		new Thread(()->{
			for(int i=0;i<10;i++) {
				strList.add(UUID.randomUUID().toString().substring(0,4));
				System.out.println(Thread.currentThread().getName()+"=>"+strList);
			}
		},"C").start();	
	}
}

  • 报错:java.util.ConcurrentModificationException(并发修改异常)

  • 解决方法:

    1. 使用List strList = new Vector<>();来解决

      • public synchronized boolean add(E e) {
            modCount++;
            add(e, elementData, elementCount);
            return true;
        }
        
      • 可见Vector的add方法使用的是synchronized来实现的线程安全的

    2. 使用List strList = Collections.synchronizedList(new ArrayList<>());来解决

      • public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        }
        
      • 可见Collections.synchronizedList()方法使用的是synchronized来实现线程安全的

    3. 使用List strList = new CopyOnWriteArrayList<>();来解决

      • public boolean add(E e) {
            synchronized (lock) {
                Object[] es = getArray();
                int len = es.length;
                es = Arrays.copyOf(es, len + 1);
                es[len] = e;
                setArray(es);
                return true;
            }
        }
        
      • 使用的是同步代码块实现的,效率要比同步方法高,同步方法直接在方法上加synchronized实现加锁,同步代码块则在方法内部加锁,很明显,同步方法锁的范围比较大,而同步代码块范围要小点,一般同步的范围越大,性能就越差,一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好

      • 所以一般使用此方法来解决集合的并发问题,效率更高

传统的Set也是不安全的

  • 解决方法

    1. 使用Set strList = Collections.synchronizedSet(new HashSet<>());来解决

    2. 使用Set strList = new CopyOnWriteArraySet<>();来解决

      • private boolean addIfAbsent(E e, Object[] snapshot) {
            synchronized (lock) {
                Object[] current = getArray();
                int len = current.length;
                if (snapshot != current) {
                    // Optimize for lost race to another addXXX operation
                    int common = Math.min(snapshot.length, len);
                    for (int i = 0; i < common; i++)
                        if (current[i] != snapshot[i]
                            && Objects.equals(e, current[i]))
                            return false;
                    if (indexOfRange(e, current, common, len) >= 0)
                        return false;
                }
                Object[] newElements = Arrays.copyOf(current, len + 1);
                newElements[len] = e;
                setArray(newElements);
                return true;
            }
        }
        
      • 本质是调用了CopyOnWriteArrayList中的方法来实现的

  • 扩展:hashset的是通过hashmap来实现的

    • public boolean add(E e) {
          return map.put(e, PRESENT)==null;
      }
      
    • 通过map的键的唯一性来实现set集合中元素的唯一性

Map

传统的HashMap集合是不安全的

  • 解决方法:
    1. 使用Map<String,String> strList = new ConcurrentHashMap<>();来解决

Callable(第三种线程启动的方式)

  • 与其他的Runnable的区别

    1. Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的
    2. Callable规定的方法是call(),Runnable规定的方法是run()
    3. Callable的任务执行后可返回值,而Runnable的任务是不能返回值(是void)
    4. call方法可以抛出异常,run方法不可以
    5. 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
    6. 加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法。
  • public class Arr {
    	
    	
    	public static void main(String[] args) {
    		
    		Map<String,String> strList = new ConcurrentHashMap<>();
    		
    		
    		MyThread myThread = new MyThread();
    		FutureTask futureTask = new FutureTask(myThread);
    		
    		new Thread(futureTask).start();
    		try {
                //此方法因为等待执行结果可能会产生阻塞,所以一般都将它放在代码最后,或者使用异步通信的解决
    			String s = (String)futureTask.get();
    			System.out.println(s);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		} catch (ExecutionException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}	
    	}
    }
    
    class MyThread implements Callable<String>{
    
    	@Override
    	public String call() throws Exception {
    		// TODO Auto-generated method stub
    		System.out.println("call");
    		return "end";
    	}
    	
    }
    
  • 注意点:

    1. 如果使用Callable来启动多个线程,只会打印一次call,原因在于Callable有缓存
    2. get()方法如果不放在最后会产生阻塞

常见的JUC并发编程辅助类

CountDownLatch(减法计数器)

  • 指定规定的总时间,调用countDown()方法来减一,CountDownLatch对象的await()方法可以造成阻塞,直到CountDownLatch减到0为止。
package demo1;

import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask;

public class CountDown {

	public static void main(String[] args) throws InterruptedException {
		CountDownLatch countDownLatch = new CountDownLatch(5);

		for (int i = 0; i < 5; i++) {
			new Thread(new FutureTask<>(new Callable<String>() {

				@Override
				public String call() throws Exception {
					// TODO Auto-generated method stub
					countDownLatch.countDown();
					System.out.println(Thread.currentThread().getName() + "Go out");
					return "success";
				}
			}), String.valueOf(i)).start();
		}
		
		//阻塞线程,直到CountDownLatch减到0为止,才唤醒,执行下面的线程
		countDownLatch.await();
		System.out.println("全部的线程执行完毕");
	}

}

适用场景

  • 秒杀
package demo1;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.FutureTask;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class CountDown {

	public static void main(String[] args) throws InterruptedException, BrokenBarrierException {

		CountDownLatch countDownLatch = new CountDownLatch(1);
		MyThread1 myThread1 = new MyThread1(countDownLatch);

		for (int i = 0; i < 1000; i++) {
			new Thread(myThread1, String.valueOf(i)).start();
		}

		System.out.println("即将开启10秒秒杀");
		for (int i = 1; i < 11; i++) {
			TimeUnit.SECONDS.sleep(1);
			System.out.println(i);
		}
		countDownLatch.countDown();

	}

}

class MyThread1 implements Runnable {

	CountDownLatch countDownLatch = null;

	public MyThread1(CountDownLatch countDownLatch) {
		this.countDownLatch = countDownLatch;
	}

	private List<String> productors = new CopyOnWriteArrayList<>(Arrays.asList("围巾", "苹果", "代码"));
	Lock lock = new ReentrantLock();
	int count = 3;

	@Override
	public void run() {
		// TODO Auto-generated method stub

		try {
			countDownLatch.await();
		} catch (InterruptedException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}

		lock.lock();
		try {
			int random = new Random().nextInt(count);
			System.out.println("用户" + Thread.currentThread().getName() + "抢到了" + productors.get(random));
			productors.remove(random);
			count--;
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			lock.unlock();
		}

	}

}

总结

  • 指定一定时间后所有线程才开始一起执行

CyclicBarrier(屏障,到达指定数量的线程参与后,所有线程执行指定的同步方法)

  • 可循环栏珊

  • CyclicBarrier构造器

    • public CyclicBarrier(int parties) {
          this(parties, null);
      }
       
      public CyclicBarrier(int parties, Runnable barrierAction) {
          if (parties <= 0) throw new IllegalArgumentException();
          this.parties = parties;
          this.count = parties;
          this.barrierCommand = barrierAction;
      }
      
    • 第一个参数是参与的线程数,每一个线程调用cyclicBarrier.await()方法来告诉CyclicBarrier自己已经到达CyclicBarrier这个屏障,当前线程被阻塞,直到达到指定数量的线程调用的次数,开始优先执行第二个参数中的同步方法

    • 第二个构造方法有一个 Runnable 参数,这个参数的意思是,线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。

package demo1;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.FutureTask;

public class CountDown {

	public static void main(String[] args) throws InterruptedException, BrokenBarrierException {

		CyclicBarrier cyclicBarrier = new CyclicBarrier(4, () -> {
			System.out.println("召唤神龙");
		});

		for (int i = 0; i < 4; i++) {
			new Thread(() -> {
				System.out.println(Thread.currentThread().getName() + "收集了一颗龙珠");
				try {
					cyclicBarrier.await();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (BrokenBarrierException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}, String.valueOf(i)).start();
		}

	}

}

CyclicBarrier和CountDownLatch的区别

  1. CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;
  2. CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断;
  3. CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。

适用场景

  • 一群小伙伴到一个餐厅点菜吃饭,有些来得慢,有些来得快,全到齐了才开始一起商量点菜

总结:

  • 在所有线程到齐之后先做一些事情后才开始一起执行

Semaphore(线程得到许可才可以执行且许可证有限)

package demo1;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.FutureTask;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class CountDown {

	public static void main(String[] args) throws InterruptedException, BrokenBarrierException {

        //规定执行许可证只有3个
		Semaphore semaphore = new Semaphore(3);
		
		for(int i=0;i<6;i++) {
			
			new Thread(()->{
				
				try {
                    //获得执行许可证
					semaphore.acquire();
					//执行线程代码
					System.out.println(Thread.currentThread().getName()+"得到执行权");
					
					TimeUnit.SECONDS.sleep(2);
					
					System.out.println(Thread.currentThread().getName()+"执行结束");
					
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}finally {
					//释放执行权
					semaphore.release();
				}
			
				
			},String.valueOf(i)).start();
		}

	}

}
  • 作用:
    1. 限流,控制最大的线程数

适用场景

  • 限流,控制最大的线程数

总结:

  • 规定最大的线程执行数;

ReadWriteLock(读写锁)

  • 适合更加细粒度的读写操作

  • 写锁(独占锁),写锁每次只能有一个线程占有

  • 读锁(共享锁),读锁可以有多个线程同时占有

示例

package demo1;

import java.util.HashMap;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class WriteReadLockDemo {

	public static void main(String[] args) {
		MyCache myCache = new MyCache();

		for (int i = 1; i < 6; i++) {
			new Thread(() -> {
				myCache.write();
			}, String.valueOf(i)).start();
		}

		for (int i = 1; i < 6; i++) {
			new Thread(() -> {
				myCache.read();
			}, String.valueOf(i)).start();
		}

	}

}

class MyCache {

	private volatile HashMap<String, String> hashMap = new HashMap<>();
	private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

	// 读
	public void read() {

		try {
			readWriteLock.readLock().lock();
			System.out.println(Thread.currentThread().getName() + "开始读取");
			hashMap.get(String.valueOf(new Random().nextInt(10)));
			System.out.println(Thread.currentThread().getName() + "读取完成");
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			readWriteLock.readLock().unlock();
		}

	}

	// 写
	public void write() {

		try {
			readWriteLock.writeLock().lock();
			System.out.println(Thread.currentThread().getName() + "开始写入");
			hashMap.put(String.valueOf(new Random().nextInt(10)), UUID.randomUUID().toString());
			System.out.println(Thread.currentThread().getName() + "写入成功");
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			readWriteLock.writeLock().unlock();
		}

	}

}

阻塞队列(BlockingQueue)

  • 队列+阻塞 就可以实现阻塞队列(自己利用LinkArrayList队列和锁也可以实现阻塞队列)

  • 关于阻塞队列的好文----->“阻塞队列”水很深,你把握不住! - 知乎 (zhihu.com)

  • 如果队列不能再写入东西了,也就是队列满了,则开始阻塞队列

  • 如果队列没有东西了,对列的取的那头开始阻塞队列,直到队列中有东西

  • queue

  • blockingqueue

  • 队列的四组api

ArrayBlockingQueue

package demo1;

import java.util.concurrent.ArrayBlockingQueue;

public class MyQueue {

	// 利用阻塞队列实现消费者生产者模式

	public static void main(String[] args) {
		MyTread2 myTread2 = new MyTread2();

		new Thread(() -> {
			myTread2.put();
		}, "生产者").start();

		new Thread(() -> {
			myTread2.get();
		}, "消费者").start();

	}

}

class MyTread2 {

	ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(4);

	// 生产者

	public void put() {
		try {
			while (true) {
				arrayBlockingQueue.put("烤鸡");
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	// 消费者
	public void get() {

		try {
			while (true) {
				System.out.println(arrayBlockingQueue.take());
			}

		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}

SynchronousQueue(阻塞队列中的一个特殊队列->同步队列)

  • 该同步队列没有大小,随存随取
package demo1;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

public class MyQueue {

	// 利用阻塞队列实现消费者生产者模式

	public static void main(String[] args) {
		MyTread2 myTread2 = new MyTread2();

		new Thread(() -> {
			myTread2.put();
		}, "生产者").start();

		new Thread(() -> {
			myTread2.get();
		}, "消费者").start();

	}

}

class MyTread2 {

	SynchronousQueue<Integer> arrayBlockingQueue = new SynchronousQueue<>();

	// 生产者

	public void put() {
		try {
			for(int i=0;i<10;i++) {
				
				arrayBlockingQueue.put(Integer.valueOf(i));
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	// 消费者
	public void get() {

		try {
			for(int i=0;i<10;i++) {
				TimeUnit.SECONDS.sleep(1);
				System.out.println(Thread.currentThread().getName()+arrayBlockingQueue.take());
			}

		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}
  • 应用:
    • 线程池

线程池

  • 由于程序运行开启线程,需要消耗系统资源,于是出现了池化技术来优化,减轻每一次获取系统资源的消耗
    • 池化技术的应用:
      • 线程池,数据库连接池,http连接池
  • 线程池的优点:
    • 降低了系统资源的消耗:通过反复利用已经创建好的线程降低反复创建和销毁造成的消耗
    • 提高了系统的响应速度:通过事先准备好的线程,避免等待线程的创建消耗的时间
    • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池技术可以进行统一的分配,监控,和调优

使用Executors工具类来创建线程池的三种方式(但是在实际开发中不会使用他来创建线程池)

//该线程池最多只可以执行一个线程
ExecutorService executor = Executors.newSingleThreadExecutor();
//该线程池最多可以有5个线程同时执行
ExecutorService executor2 = Executors.newFixedThreadPool(5);
//该线程池不固定大小
ExecutorService executor3 = Executors.newCachedThreadPool();

使用

package demo1;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

public class MyQueue {

	// 利用阻塞队列实现消费者生产者模式

	public static void main(String[] args) {
		//该线程池最多只可以执行一个线程
		ExecutorService executor = Executors.newSingleThreadExecutor();
		//该线程池最多可以有5个线程同时执行
		ExecutorService executor2 = Executors.newFixedThreadPool(5);
		//该线程池不固定大小
		ExecutorService executor3 = Executors.newCachedThreadPool();
		
				
		for(int i=0;i<10;i++) {
			executor.execute(()->{
				System.out.println(Thread.currentThread().getName()+"单个线程");
			});
		}
		
		for(int i=0;i<10;i++) {
			executor2.execute(()->{
				System.out.println(Thread.currentThread().getName()+"最多可以有5条线程同时执行");
			});
		}
		
		for(int i=0;i<10;i++) {
			executor3.execute(()->{
				System.out.println(Thread.currentThread().getName()+"最多可以有10条线程同时执行");
			});
		}
	}

}

在实际开发中使用ThreadPoolExecutor类来创建线程池

public class MyQueue {

	// 利用阻塞队列实现消费者生产者模式

	public static void main(String[] args) {

		Executors.newSingleThreadExecutor();

		/**
		 * 第一个参数:最小核心线程数 
		 * 第二个参数:最大核心线程数
		 * 第三个参数:如果队列没有满,所需要执行的线程数不多,在规定的时间后淘汰其他的线程位,只保留2个线程位 
		 * 第四个参数:规定第三个参数的时间单位
		 * 第五个参数:队列的类型 
		 * 第六个参数:默认的线程工厂 ,一般不会变
		 * 第七个参数:如果执行的线程数大于最大线程数+队列规定的最大数,所需要采用的拒绝策略
		 */
		
		/**
		 * 四大拒绝策略
		 * 第一:new ThreadPoolExecutor.AbortPolicy() 如果需要执行的线程数多于最大线程位+最大队列位,不会执行多余的线程,抛出异常
		 * 第二:new ThreadPoolExecutor.CallerRunsPolicy() 如果需要执行的线程数多于最大线程位+最大队列位,会将多余的线程交给main线程执行,不会抛出异常
		 * 第三:new ThreadPoolExecutor.DiscardPolicy() 如果需要执行的线程数多于最大线程位+最大队列位,不会执行对于的线程,也不会抛出异常
		 * 第四:new ThreadPoolExecutor.DiscardOldestPolicy() 如果需要执行的线程数多于最大线程位+最大队列位,会尝试去找空闲的线程位,如果有则执行,没有,不执行,且不会抛出异常
		 * */
		ExecutorService executor = new ThreadPoolExecutor(3, 5, 6, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3),
				Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

		try {
			for (int i = 1; i <= 20; i++) {
				executor.execute(() -> {
					System.out.println("正在执行" + Thread.currentThread().getName());
				});
			}
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		} finally {
			executor.shutdown();
		}

	}

}
  • 七大参数

    1. 第一个参数:最小核心线程数
    2. 第二个参数:最大核心线程数
    3. 第三个参数:如果队列没有满,所需要执行的线程数不多,在规定的时间后淘汰其他的线程位,只保留2个线程位
    4. 第四个参数:规定第三个参数的时间单位
    5. 第五个参数:队列的类型
    6. 第六个参数:默认的线程工厂 ,一般不会变
    7. 第七个参数:如果执行的线程数大于最大线程数+队列规定的最大数,所需要采用的拒绝策略
  • 四大拒绝策略

    1. new ThreadPoolExecutor.AbortPolicy() 如果需要执行的线程数多于最大线程位+最大队列位,不会执行多余的线程,抛出异常
    2. new ThreadPoolExecutor.CallerRunsPolicy() 如果需要执行的线程数多于最大线程位+最大队列位,会将多余的线程交给main线程执行,不会抛出异常
    3. new ThreadPoolExecutor.DiscardPolicy() 如果需要执行的线程数多于最大线程位+最大队列位,不会执行对于的线程,也不会抛出异常
    4. new ThreadPoolExecutor.DiscardOldestPolicy() 如果需要执行的线程数多于最大线程位+最大队列位,会尝试去找空闲的线程位,如果有则执行,没有,不执行,且不会抛出异常
  • 最大线程到底该如何定义

    • CPU密集型,最多有几核就设置几个,让cpu效率最高
    • IO密集型,判断应用中有多少个IO线程,将最大线程数一般设置为其2倍

四大函数式接口

  • 函数式接口Function
  • 判定式接口Predicate
  • 消费者接口Consumer
  • 供给式接口Supplier
package demo1;

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class FunctionInter {
	public static void main(String[] args) {
		//函数式接口,输入和输出
		Function<String, String> function = (String str)->{return str;};
		System.out.println(function.apply("clt"));
		//断定式接口,输入和判断返回布尔值
		Predicate<String> predicate = (String str)->{return str.isEmpty();};
		System.out.println(predicate.test("123"));
		//消费者接口,只有输入没有输出
		Consumer<String> consumer = (String str)->{System.out.println(str);};
		consumer.accept("clt");
		//供给式接口,是由输出没有输入
		Supplier<String> supplier = ()->{return "201319";};
		System.out.println(supplier.get());
		
	}
}

流式计算(Stream)(简单了解)

  • 大数据时代,集合负责存储数据,Stream负责计算数据
package demo1;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class FunctionInter {

	
	public static void main(String[] args) {
		/**
		 * 需要家乡在贵州,年龄在1岁,工作是码农的人,并将姓名转为大写
		 * */
		List<User> strList = Arrays.asList(new User("clt1", 0, "码农", "贵州"),new User("clt2", 1, "码农1", "苏州"),new User("clt3", 2, "码农", "贵州"),new User("clt4", 3, "码农", "贵州"));
		
		strList
		.stream()
		.filter(u->{return u.getHome().equals("贵州");})
		.filter(u->{return u.getAge()>=1;})
		.filter(u->{return u.getWorker().equals("码农");})
		.map(u->{return u.getUsername().toUpperCase();})
            //排序
		.sorted((u1,u2)->{return u2.compareTo(u1);})
            //限制返回数
		.limit(2)
		.forEach(System.out::println);
		
	}
}


class User {
	private String username;
	private int age;
	private String worker;
	private String home;
	
	
	public User(String username,int age,String worker,String home) {
		this.username=username;
		this.age=age;
		this.worker=worker;
		this.home=home;
	}


	public String getUsername() {
		return username;
	}


	public void setUsername(String username) {
		this.username = username;
	}


	public int getAge() {
		return age;
	}


	public void setAge(int age) {
		this.age = age;
	}


	public String getWorker() {
		return worker;
	}


	public void setWorker(String worker) {
		this.worker = worker;
	}


	public String getHome() {
		return home;
	}


	public void setHome(String home) {
		this.home = home;
	}


	@Override
	public String toString() {
		return "User [username=" + username + ", age=" + age + ", worker=" + worker + ", home=" + home + "]";
	}
}

计算大数据的方式(ForkJoin)

  • 使用普通for循环计算

  • 使用ForkJoinTask任务来计算

    • RecursiveAction:递归事件,没有返回值
    • RecursiveTask:递归任务,有返回值
    • CountedCompleter
    • ForkJoin将大任务化成小任务(Map Reduce)
    • ForkJoin的工作窃取机制:当自己的工作做完后会去将别人的任务抢过来,提高执行效率
    • ForkJoin维护的是双端队列
  • 使用Stream中的LongStream并行流来计算(效率最高)

测试

package demo1;

import java.util.Date;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;

public class FunctionInter {

	public static void main(String[] args) {

		FunctionInter functionInter = new FunctionInter();
		functionInter.test3();

	}

	// 使用普通for循环计算
	// 计算结果为500000000500000000,花费时间为4670
	public void test1() {
		Long result = 0L;
		Long startTime = System.currentTimeMillis();
		for (Long i = 1L; i <= 10_0000_0000; i++) {
			result += i;
		}
		Long endTime = System.currentTimeMillis();
		System.out.printf("计算结果为%d,花费时间为%d", result, endTime - startTime);
	}

	// 使用ForkJoinPool来计算ForkJoinTask计算任务
	// 计算结果为500000000500000000,花费时间为3608
	public void test2() {
		Long result = 0L;
		Long startTime = System.currentTimeMillis();

		ForkJoinPool forkJoinPool = new ForkJoinPool();
		Computed computed = new Computed(1L, 10_0000_0000L);
		ForkJoinTask<Long> forkJoinTask = forkJoinPool.submit(computed);
		try {
			result = forkJoinTask.get();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ExecutionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		Long endTime = System.currentTimeMillis();
		System.out.printf("计算结果为%d,花费时间为%d", result, endTime - startTime);
	}
	
	//使用流来计算结果
	//计算结果为500000000500000000,花费时间为116
	public void test3() {
		Long result = 0L;
		Long startTime = System.currentTimeMillis();
		result = LongStream.rangeClosed(1L, 10_0000_0000L).parallel().reduce(0, Long::sum);
		Long endTime = System.currentTimeMillis();
		System.out.printf("计算结果为%d,花费时间为%d", result, endTime - startTime);
	}

}

class Computed extends RecursiveTask<Long> {

	private Long startNumber = 0L;
	private Long endNumber = 0L;

	public Computed(Long startNumber, Long endNumber) {
		this.startNumber = startNumber;
		this.endNumber = endNumber;
	}

	@Override
	protected Long compute() {

		if ((endNumber - startNumber) <= 10000L) {
			Long result = 0L;
			for (Long i = startNumber; i <= endNumber; i++) {
				result += i;
			}
			return result;
		} else {

			Long media = (endNumber + startNumber) / 2;

			Computed computed = new Computed(startNumber, media);
			computed.fork();

			Computed computed2 = new Computed(media + 1, endNumber);
			computed2.fork();

			return computed.join() + computed2.join();

		}

	}

}

异步任务

  • 没有返回结果
package demo1;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class Synctest {

	public static void main(String[] args) throws InterruptedException, ExecutionException {
		//执行异步任务没有返回结果
		CompletableFuture completableFuture = CompletableFuture.runAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(2);
				System.out.println("异步任务");
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		});
		System.out.println("hh");
		completableFuture.get();	
	}
}
  • 有返回结果的异步回调
package demo1;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class Synctest {

	public static void main(String[] args) throws InterruptedException, ExecutionException {
		// 执行异步任务没有返回结果
		CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> {
			int result = 1/0;
			try {
				
 				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

			return "有返回结果的异步回调";
		});
		System.out.println("hh");
		
		System.out.println(completableFuture.whenComplete((t, u) -> {
			//第一个参数表示异步回调执行完成返回的结果
			//第二个参数表示异步回调的异常信息
			System.out.println(t + "3" + u);
		}).exceptionally((e)->{
			System.out.println(e);
			return 12;
		}).get());

	}

}

对Volatile理解(简单)

  • Volatile是Java虚拟机提供的轻量级的同步机制

特点

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

JMM

  • JMM是Java内存模型,不存在的东西,只是概念

关于JMM的一些同步的约定

  1. 线程解锁前,必须把共享的变量刷会主存
  2. 线程加锁前,必须读取主存中最新的变量信息到工作内存
  3. 加锁和解锁是同一把锁

关于线程的工作内存和主存

  • 线程和主存之间的通信

JMM中定义的八种操作

  • 锁定(lock): 作用于主内存中的变量,将他标记为一个线程独享变量。

  • 解锁(unlock): 作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。

  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。

  • load(载入):把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中。

  • use(使用):把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令。

  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。

  • write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。

同步的一些规定,保证同步操作的正确性

  • 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。

  • 一个新的变量只能在主内存中 “诞生”,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量,换句话说就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。

  • 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。

  • 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。

  • 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量。

Volatile

保证可见性

  • 没有出现可见性问题的原因

    • 发生可见性问题,需要两个条件,①各线程操作数据是在缓存中操作,才会存在缓存不一致;②没有触发缓存一致性协议。

  • 每一个线程都能及时知道主内存中数据变量的改变,继而做出相应操作

  • 测试可见性

    • package com.chen.Deom1;
      
      import java.sql.Time;
      import java.util.concurrent.TimeUnit;
      
      public class Test {
      
          private volatile static int i = 0;
      
          public static void main(String[] args) throws InterruptedException {
      
              new Thread(()->{
                  while (i==0){
      
                  }
              }).start();
      
              TimeUnit.SECONDS.sleep(1);
      
              i=2;
      
          }
      }
      
    • 没有volatile的时候程序一直卡在while,有volatile后i=2程序停止

不保证原子性

  • 测试

    • package com.chen.Deom1;
      
      import java.sql.Time;
      import java.util.concurrent.TimeUnit;
      
      public class Test {
      
          private volatile static int sum = 0;
      
          public  static void add(){
              sum++;
          }
      
          public static void main(String[] args) throws InterruptedException {
      
      
              for(int i=1;i<=20;i++){
                  new Thread(()->{
                      for(int q=1;q<=100;q++){
                          add();
                      }
                  }).start();
              }
      
              while (Thread.activeCount()>2){
                  Thread.yield();
              }
      
              System.out.println(sum);
          }
      }
      
    • 结果可能不为2000,所以不保证原子性

    • 虽然add方法中只有sum++一步操作,但是为什么不能保证原子性呢

      • sum++的字节码执行需要多条指令,有拿出变量,赋值++,返回变量等,这些都是可以被多个线程共享的,所以底层不是一步

如何在不使用同步方法和Lock锁的前提下保证原子性

  • 使用JUC下的工具类
  • java.util.concurrent.atomic:改包下的类直接和操作系统有关,在内存中直接修改数据,unsafe是一个很特殊的类

  • 使用原子类AtomicInteger解决
package com.chen.Deom1;

import java.sql.Time;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Test {

    private static AtomicInteger sum = new AtomicInteger();

    public  static void add(){
        sum.getAndIncrement();
    }

    public static void main(String[] args) throws InterruptedException {


        for(int i=1;i<=20;i++){
            new Thread(()->{
                for(int q=1;q<=100;q++){
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println(sum);
    }
}

指令重排

  • 熟知的的程序逻辑可能在执行的时候不是按照编码时候的逻辑顺序执行的

  • int x=1;
    int y=2;
    int sum = x+y;
    //我们所想象的执行顺序是按照编码时候的逻辑顺序执行的
    
  • int y=1;
    int x=2;
    int sum = x+y;
    //但是经过指令重排后可能变成这样
    

指令重排可能会导致结果的不同(但是几率很小)

  • 如,x,y,a,b的初始化值都是0
线程一 线程二
int x=1 int y=2
int a=y int b=x

第一种结果可能是a=1,b=1,但是如果经过指令重排后

线程一 线程二
int a=y int b=x
int x=1 int y=2

第二种结果可能是a=0,b=0

如何避免指令重排

  • 使用volatile关键字来加内存屏障,避免指令重排

volatile关键字使用最多的地方

  • 在单例模式中使用得最多(DCL懒汉式)

单例模式

饿汉式

public class Test {
    //构造器私有化
    private Test(){};

    private final static  Test test = new Test();

    public static Test getInstance(){
        return test;
    }
}

懒汉式

public class Test2 {
    
    private Test2() {}

    //当然还需要加volatile避免指令重排,导致先执行分配空间,再初始化对象,导致的一系列问题
    private volatile static Test2 test2;

    public static Test2 getInstance() {
        //双重检测锁模式,安全,避免在多线程创建单例失败,原因在于创建对象的时候底层分为初始化,分配空间等操作
        if (test2 == null) {
            synchronized (Test2.class) {
                if (test2 == null) {
                    test2 = new Test2();
                }
            }
        }
        return test2;
    }
}

静态内部类

public class Test3 {

    private Test3(){}

    private Test3 getInstance(){
        return InnerClass.test3;
    }

    public static class InnerClass{
        private static final Test3 test3 = new Test3();
    }

}

不管是饿汉式还是懒汉式都存在问题

  • 可以使用反射来破坏单例模式

  • 以饿汉式测试

    • package com.chen.Deom1;
      
      import java.lang.reflect.Constructor;
      import java.sql.Time;
      import java.util.concurrent.TimeUnit;
      import java.util.concurrent.atomic.AtomicInteger;
      
      public class Test {
      
          //构造器私有化
          private Test(){};
      
          private final static  Test test = new Test();
      
          public static Test getInstance(){
              return test;
          }
      
      
          public static void main(String[] args) throws Exception {
      
              Test instance = Test.getInstance();
              Test instance1 = Test.getInstance();
              System.out.println(instance.hashCode());
              System.out.println(instance1.hashCode());
      
              Constructor<Test> declaredConstructor = Test.class.getDeclaredConstructor(null);
              declaredConstructor.setAccessible(true);
              Test test = declaredConstructor.newInstance(null);
              Test test1 = declaredConstructor.newInstance(null);
              System.out.println(test1.hashCode());
              System.out.println(test.hashCode());
      
          }
      }
      
    • 结果是创建出来的对象不是同一个

  • 解决方法:使用枚举类来实现单例类

    • package com.chen.Deom1;
      
      import java.lang.reflect.Constructor;
      
      public enum Test3 {
          //定义枚举变量
          Singleton;
      
          public Test3 getInstance(){
              return Singleton;
          }
      
          private int i=1;
          private int q=2;
      
          public int add(){
              return i+q;
          }
      
      }
      
      class Test4{
          public static void main(String[] args) throws Exception{
              Test3 test3 = Test3.Singleton.getInstance();
              Test3 test4 = Test3.Singleton.getInstance();
              System.out.println(test3==test4);
              Constructor<Test3> declaredConstructor = Test3.class.getDeclaredConstructor(String.class,int.class);
              declaredConstructor.setAccessible(true);
              Test3 test31 = declaredConstructor.newInstance();
              System.out.println(test31.hashCode());
          }
      }
      

使用枚举类实现单例类

  • 好处:

    • 天然线程安全
    • JVM保证全局唯一
    • 不能被反射机制创建
  • 为什么枚举天然线程安全

    • 我们定义的一个枚举, 在第一次被真正用到的时候, 会被虚拟机加载并初始化, 而这个初始化过程是线程安全的。

    而我们知道, 解决单例的并发问题, 主要解决的就是初始化过程中的线程安全问题

    • 枚举中的所有属性都被static修饰,天然支持多线程

    • static 类型的属性会在类加载过程初始化, 当一个 Java 类第一次被真正使用到的时候静态资源被初始化、 Java 类的加载和初始化过程都是线程安全的( 因为虚拟机在加载枚举的类的时候, 会使用 ClassLoader 的 loadClass 方法, 而这个方法使用同步代码块保证了线程安全)

CAS(比较并交互)

  • AtomicInteger类的compareAndSet方法就是调用了Unsafe类中的本地方法compareAndSwapInt来操作内存实现比较交换的

    public static void main(String[] args) throws Exception {
        AtomicInteger atomicInteger = new AtomicInteger(2023);
        //CAS(java层面是compareAndSet)
        boolean b = atomicInteger.compareAndSet(2023, 2024);
        System.out.println("是否修改成功"+b+"\t修改为了"+atomicInteger.get());
    }
    
  • Unsafe类中包含大量的本地方法

  • AtomicInteger类中的incrementAndGet方法也是调用Unsafe类中的本地方法getAndAddInt的

    //AtomicInteger类
    private static final long valueOffset;
    
    static {
        try {
            //通过调用unsafe中的本地方法,利用反射获取使用的AtomicInteger对象中的value这个属性,得到在AtomicInteger对象中的偏移量,定位value在AtomicInteger中的位置
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    
    
    public final int incrementAndGet() {
        //调用Unsafe类中的方法将AtomicInteger对象和value的偏移量传给Unsafe类
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    
    //该方法是的JNI(本地方法接口)是通过一条CPU指令实现的,所以是原子性的,所以保证了下面方法的原子性
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //该方法用于获取指定对象中指定偏移量处的属性的值,并保存到var5中
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    	//这边使用了CAS来判断刚刚获取的var5的值是否还是原来的那个value(以此来保证只有当前线程进行操作,来保证对value操作的原子性),如果是则加一,否则进入下一次循环(自旋锁),重新获得最新的value值
        return var5;
    }
    
  • CAS的缺点

    1. 循环耗时
      • 如果一直自旋,会给CPU带来非常大的执行开销
    2. 一次性只能保证一个共享变量的原子性
      • 对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
      • Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
    3. ABA问题

ABA问题(使用乐观锁解决)

  • CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是CAS的ABA问题。

  • 解决方法(对每一次的操作加上版本号):

    • 常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
  • 代码

    • package com.chen.Deom1;
      
      import java.sql.Time;
      import java.util.concurrent.TimeUnit;
      import java.util.concurrent.atomic.AtomicStampedReference;
      
      public class Test {
      
          public static void main(String[] args) throws Exception {
      
              AtomicStampedReference<Integer> integerAtomicStampedReference = new AtomicStampedReference<>(1,1);
      
      
              new Thread(()->{
      
                  int stamp = integerAtomicStampedReference.getStamp();
                  System.out.println(Thread.currentThread().getName()+"初始的版本号为"+stamp);
                  //每一次改变成功版本号都会变
                  boolean b = integerAtomicStampedReference.compareAndSet(1, 2, integerAtomicStampedReference.getStamp(), integerAtomicStampedReference.getStamp() + 1);
                  System.out.println(Thread.currentThread().getName()+b);
                  boolean b1 = integerAtomicStampedReference.compareAndSet(2, 1, integerAtomicStampedReference.getStamp(), integerAtomicStampedReference.getStamp() + 1);
                  System.out.println(Thread.currentThread().getName()+b1);
                  System.out.println(integerAtomicStampedReference.getStamp());
                  try {
                      TimeUnit.SECONDS.sleep(1);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              },"A").start();
      
              new Thread(()->{
                  int stamp = integerAtomicStampedReference.getStamp();
                  System.out.println(Thread.currentThread().getName()+"初始的版本号为"+stamp);
                  boolean b = integerAtomicStampedReference.compareAndSet(1, 3, integerAtomicStampedReference.getStamp(), integerAtomicStampedReference.getStamp() + 1);
                  System.out.println(Thread.currentThread().getName()+b);
              },"B").start();
          }
      }
      
    • 注意事项:

可重入锁

  • 理解:如果一个锁里面还有一把锁,那么当拿到最外面的一把锁就会自动拿到里面的锁,直到最外面的锁释放后,里面的锁才释放
public void m() {
    lock.lock();
    lock.lock();
    try {
      // ... method body
    } finally {
      lock.unlock()
      lock.unlock()
    }
}
//代码理解:当最里面的锁的程序执行完成后,也不会释放锁,只有最外面的锁释放后,里面的锁才能释放

Synchronized关键字锁也是可重入锁

public static void main(String[] args) {
    Test5 test5 = new Test5();
    new Thread(()->{test5.sms();},"A").start();
    new Thread(()->{test5.sms();},"B").start();
}

public synchronized void sms(){
    System.out.println(Thread.currentThread().getName()+"发短信");
    call();
}

public synchronized void call(){
    System.out.println(Thread.currentThread().getName()+"打电话");
}

//执行结果
A发短信
A打电话
    
B发短信
B打电话

Lock的可重入锁

public static void main(String[] args) {
    Test5 test5 = new Test5();
    new Thread(()->{test5.sms();},"A").start();
    new Thread(()->{test5.sms();},"B").start();
}

Lock lock = new ReentrantLock();

public void sms(){
    lock.lock();
    //细节问题,锁必须一对一对出现
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName()+"发短信");
        call();
    }finally {
        //加了几把锁就需要解开几把锁,否则出现死锁问题
        lock.unlock();
        lock.unlock();
    }
}

public void call(){
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName()+"打电话");
    }finally {
        lock.unlock();
    }
}
//执行结果
A发短信
A打电话
    
B发短信
B打电话

自旋锁(使用CAS)

  • 自定义自旋锁
package com.chen.Deom1;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class Test6 {


    public static void main(String[] args) {

        MyLock myLock = new MyLock();
        for(int j=1;j<=3;j++){
            new Thread(()->{
                try {
                    myLock.myLock();
                    for(int i=0;i<5;i++){
                        try {
                            System.out.println(Thread.currentThread().getName()+"正在使用锁");
                            TimeUnit.SECONDS.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                } finally {

                    try {
                        System.out.println(Thread.currentThread().getName()+"解锁中");
                        for(int i=0;i<3;i++){
                            System.err.println(".");
                            TimeUnit.MILLISECONDS.sleep(500);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    myLock.myUnLock();
                }
            },String.valueOf(j)).start();
        }

    }
}

class MyLock{

    AtomicReference<Thread> threadAtomicReference = new AtomicReference<>();

    public void myLock(){
        Thread thread = Thread.currentThread();
        while(!threadAtomicReference.compareAndSet(null,thread)){}
    }

    public void myUnLock(){
        Thread thread = Thread.currentThread();
        threadAtomicReference.compareAndSet(thread,null);
    }
}

死锁排查

  • 自定义死锁
package com.chen.Deom1;

import java.sql.Time;
import java.util.concurrent.TimeUnit;

public class Test7 {


    public static void main(String[] args) {

        String i1="1";
        String i2="2";

        new Thread(new R(i1,i2),"A").start();
        new Thread(new R(i2,i1),"B").start();

    }

}

class R implements Runnable{

    private String i1;
    private String i2;

    public R(String i1,String i2){
        this.i1=i1;
        this.i2=i2;
    }

    @Override
    public void run() {
        synchronized (i1){
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (i2){

            }
        }
    }
}
  • 排除死锁

    • 使用jps命令查看所有的java进程
    • 使用jstack 进程号 查看死锁信息
  • 工作中排查问题

    • 查看日志
    • 查看堆栈信息(重要)
posted @ 2023-08-03 22:19  在赶路的我  阅读(9)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3