学习自b站狂神说
提示:以下是本篇文章正文内容,下面案例可供参考
进程:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个基本单位。
线程:
java真的可以开启线程吗? 不可以。
java调用本地方法开启线程,底层是C++。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
//本地方法,底层C++,java无法直接操作硬件(java是运行在虚拟机上的)
private native void start0();
并发:(多个线程操作同一个资源)
CPU一核,模拟出来多条线程。
并行:(多个人一起行走)
CPU多核,多个线程可以同时执行。
并发编程的本质:充分利用CPU的资源(所有公司都看重)
以卖票为例,多个线程买票,需要排队获取锁,保证线程安全。代码如下:
public class SaleTestDemo {
public static void main(String[] args) {
//并发,多个线程操作同一个资源类
Ticket ticket = new Ticket();
//Runnable 函数式接口 jdk1.8 Lambda 表达式
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"C").start();
}
}
//资源类 OOP
class Ticket {
//属性 方法
private int number = 30;
//卖票的方式
//synchronized 本质: 队列 锁
public synchronized void sale(){
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "票,剩余:" + number);
}
}
}
代码如下:
class Ticket2 {
//属性 方法
private int number = 30;
Lock lock = new ReentrantLock(true); //默认:false 非公平锁
//卖票的方式
public void sale(){
lock.lock(); //加锁
lock.tryLock(); //尝试获取锁
try {
//业务代码
}
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock(); //解锁
}
}
}
new ReentrantLock(true); //默认:false 非公平锁
// 可重入锁(普通锁)
new ReentrantLock
// 读锁
ReentrantReadWriteLock.ReadLock
// 写锁
ReentrantReadWriteLock.WriteLock
线程通信问题:生产者消费者问题。生产线程操作完毕后通知消费线程去消费,不满足生产条件时 生产线程处于等待状态。消费线程消费完成后,通知生产线程。
虚假唤醒:线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒 。 虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并且如果条件不满足则继续等待
class Data{
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
if (number != 0) {
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
if (number != 1) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
this.notifyAll();
}
}
一个生产者,一个消费者情况下。A + 1 ,B - 1 。使用if 和synchronized 没有问题。
但是,两个生产者,两个消费者时,就会出现大于1,或者小于0的情况,还可能造成死锁。
解决方法如下:
class Data2{
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//+1
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
condition.await(); //等待
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
condition.signalAll(); //唤醒
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//-1
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
condition.await(); //等待
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
condition.signalAll(); //唤醒
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
Condition condition = lock.newCondition();
condition.await(); //等待
condition.signalAll(); //唤醒
使用while 和 lock 锁,在while条件满足时,等待唤醒,执行完后在用signalAll() 唤醒其他线程。
如果要专门唤醒某个线程,则需要线程对应的Condition 对象调用 signal() 方法。
代码如下:
private Condition condition1 = lock.newCondition(); // A
private Condition condition2 = lock.newCondition(); // B
private Condition condition3 = lock.newCondition(); // C
private Condition condition4 = lock.newCondition(); // D
A线程业务执行完成后,调用condition2.signal(); B线程被唤醒。
public class TestDemo02 {
public static void main(String[] args) throws InterruptedException {
Phone2 phone = new Phone2();
new Thread(()->{ phone.sendEmail(); },"A").start();
//sleep 1s
TimeUnit.SECONDS.sleep(1);
new Thread(()->{ phone.call();},"B").start();
new Thread(()->{ phone.hello();},"C").start();
}
}
class Phone2{
public synchronized void sendEmail() {
try { //延迟4S
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发邮件");
}
public synchronized void call() {
System.out.println("打电话");
}
public void hello() { //不加锁
System.out.println("hello");
}
}
hello
发邮件
打电话
class Phone3{
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发邮件");
}
public synchronized void call() {
System.out.println("打电话");
}
}
打电话
发邮件
public class TestDemo03 {
public static void main(String[] args) throws InterruptedException {
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
new Thread(()->{ phone1.sendEmail(); },"A").start();
//sleep 1s
TimeUnit.SECONDS.sleep(1);
new Thread(()->{ phone2.call();},"B").start();
}
}
class Phone3{
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发邮件");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
发邮件
打电话
//并发下,ArrayList不安全,写入操作时,会发生并发修改异常
//java.util.ConcurrentModificationException
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
解决方案:
* 1. List list = new Vector<>();
* 2. List list = Collections.synchronizedList(new ArrayList<>());
* 3. JUC List list = new CopyOnWriteArrayList<>();
Vector的原理是synchronized锁。
Collections.synchronizedList(new ArrayList<>()):把ArrayList变安全。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
CopyOnWrite: 写入时复制: 在写入前先复制一份,将元素插入到复制的list中,然后再将插入后的list写入到原来的list中。 在写入时避免覆盖,造成数据问题。
HashSet 底层 :Hashmap (key是无法重复的)
ConcurrentHashMap的原理:
1.7版本: 使用锁分段技术。
数据结构:ReentrantLock + Segment数组 + HashEntry 数组, 每个HashEntry的元素又是一个链表。
1.8版本 CAS操作 + synchronized锁 + Node + 红黑树。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总计数是6 必须要执行的任务的时候使用
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "go out");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await(); //等待计数器归0,然后再向下执行
System.out.println("close door");
}
}
public class CyclicBarrierDemo {
public static void main(String[] args) {
//召唤神龙的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙");
});
for (int i = 0; i < 7; i++) {
final int temp = i;
int finalI = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "收集" + temp + "个龙珠");
try {
cyclicBarrier.await(); //等待 加法计数器达到7
System.out.println("释放线程" + finalI);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
Thread-1收集1个龙珠
Thread-0收集0个龙珠
Thread-6收集6个龙珠
Thread-5收集5个龙珠
Thread-2收集2个龙珠
Thread-4收集4个龙珠
Thread-3收集3个龙珠
召唤神龙
释放线程5
释放线程2
释放线程4
...
设定值为7,当等待的线程达到7个时,执行特定方法,然后释放7个线程。
一个计数信号量。
acquire():从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。
代码如下:
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量 停车位 限流
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
//acquire() 得到车位
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
semaphore.acquire(); 获得,如果已经满了,等待,等到被释放为止。
semaphore.release(); 释放,会将当前的信号量释放+1,然后唤醒等待的线程。
作用:
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//写入
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp + "",temp + "");
},String.valueOf(i)).start();
}
//读取
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp + "");
},String.valueOf(i)).start();
}}}
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
//存、写
public void put(String key,Object value) {
System.out.println(Thread.currentThread().getName() + "写入");
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入OK");
}
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "读取");
map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
}
}
3写入
4写入
4写入OK
未使用读写锁时,在线程3 写入的过程中,有其他线程插入。存在线程安全问题。
使用读写锁的代码如下:put 方法,get一样
//读写锁 更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//存、写入的时候,只希望同时只有一个线程写
public void put(String key,Object value) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
readWriteLock.writeLock().lock(); 写锁
readWriteLock.readLock().lock(); 读锁
三个主要的实现类
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
什么情况会使用 阻塞队列:多线程并发处理,线程池。
public static void main(String[] args) {
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() + ": put 1");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName() + ": put 2");
synchronousQueue.put("2");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"s1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + ": take" + synchronousQueue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + ": take" + synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"s1").start();
}
s1: put 1
s1: take1
s1: put 2
s1: take2
结论: put 执行完后,必须等待take()方法执行,才能继续put。
使用线程池的好处:
ExecutorService threadPool = Executors.newSingleThreadExecutor(); //单个线程
ExecutorService threadPool = Executors.newFixedThreadPool(5); //创建一个固定数量的线程池
ExecutorService threadPool = Executors.newCachedThreadPool(); //可伸缩的
ExecutorService threadPool = new ThreadPoolExecutor(
2, // 线程池核心线程大小
5, //线程池最大线程数量
3, //空闲线程存活时间
TimeUnit.SECONDS, // 空闲线程存活时间单位
new LinkedBlockingDeque<>(3), //等待区空间 3
Executors.defaultThreadFactory(), //线程工厂
new ThreadPoolExecutor.DiscardOldestPolicy()//拒绝策略
);
拒绝策略是第七个参数。
* 四个拒绝策略:
* 1. new ThreadPoolExecutor.AbortPolicy() //线程池和等待空间都满了,还有线程进来,不处理,抛出异常
*
* 2. new ThreadPoolExecutor.CallerRunsPolicy() //哪来的 回哪里去。
*
* 3. new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常,
*
* 4. new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早进来的线程竞争。
//获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());
判断你的程序中十分耗IO的线程数。一般设置为2倍。
假设:1个程序,15个大型任务 IO十分占用资源
@FunctionInterface
函数式接口:只有一个方法的接口,一个参数,一个返回值
public static void main(String[] args) {
Function function = new Function<String,String>(){
@Override
public String apply(String o) {
return o;
}
};
//Lambda表达式简化
Function<String,String> function1 = (str)->{
return str;
};
}
输出:abc
断定型接口:有一个输入参数,返回值只能是布尔值。
public static void main(String[] args) {
//判断字符是否为空
// Predicate predicate = new Predicate<>() {
// @Override
// public boolean test(String s) {
// return s.isEmpty();
// }
// };
Predicate<String> predicate = (str)->{return str.isEmpty();};
System.out.println(predicate.test(""));
}
输出:true
public static void main(String[] args) {
Consumer<String> consumer = new Consumer() {
@Override
public void accept(Object o) {
System.out.println(o);
}
};
//Lambda表达式简化
Consumer<String> consumer1 = (str)->{System.out.println(str);};
consumer.accept("sad");
consumer1.accept("hello");
}
输出:
sad
hello
##4. 供给型接口
public static void main(String[] args) {
Supplier supplier = new Supplier() {
@Override
public Object get() {
System.out.println("get()");
return 1024;
}
};
//Lambda表达式简化
Supplier supplier1 = ()->{
System.out.println("get");
return 100;
};
supplier.get();
supplier1.get();
}
输出:
get()
get
public class Hungry {
//可能会浪费空间
private byte[] data1 = new byte[1024];
//构造方法
private Hungry() {};
//实例化对象
private final static Hungry HUNGRY = new Hungry();
//获取对象
public static Hungry getInstance() {
return HUNGRY;
}
public static void main(String[] args) {
System.out.println(Hungry.getInstance(););
}
}
public class Lazy {
private Lazy () {}
private static Lazy LAZY;
public static Lazy getInstance() {
if (LAZY == null) {
LAZY = new Lazy();
}
return LAZY;
}
public static void main(String[] args) {
System.out.println(Lazy.getInstance());
}
}
原因: 由于new 操作不是一个原子性操作,可能会发生指令重排。
new 指令:
public class DoubleLock {
//volatile :防止指令重排
private volatile static DoubleLock LazyMan2;
public DoubleLock (){}
//双重检测模式
public static DoubleLock getInstance() {
if (LazyMan2 == null) {
synchronized (DoubleLock.class) {
if (LazyMan2 == null) {
LazyMan2 = new DoubleLock();
}
}
}
return LazyMan2;
}
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//反射获取对象
Constructor<DoubleLock> declaredConstructor = DoubleLock.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
DoubleLock doubleLock = declaredConstructor.newInstance();
DoubleLock instance = DoubleLock.getInstance();
System.out.println(doubleLock);
System.out.println(instance);
}
com.sparks.single.DoubleLock@7c30a502
com.sparks.single.DoubleLock@49e4cb85
//静态内部类 单例
public class Holder {
public Holder(){};
public static Holder getInstance() {
return InnerClass.HOLDER;
}
public static class InnerClass {
private static final Holder HOLDER = new Holder();
}
}
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
除了枚举类,其他的单例模式,都会被反射破坏。
Unsafe类: Unsafe是CAS核心类。Java无法直接访问底层操作系统,而是通过本地方法访问。JDK中Unsafe类,底层调用本地方法,提供硬件级别原子操作。
1. ABA问题: CAS在操作值的时候会检测值是否发生变化。如果没有变化,则进行更新。但是,如果一个值由A变成了B,又由B变回了A,这个时候CAS检查就会判断为值没有发生变化,但实际上已经发生了变化。
解决方法: 在变量前面追加版本号,每次变量更新的时候,将版本号+1,这时候A->B->A就变成1A->2B->3A,CAS检查值的时候就发现值发生了变化。
2. 循环时间长开销大