多线程
多线程就是Java中一个程序分成多个线程共同执行,线程是程序的最小运行单位。线程的调用是根据CPU调用来排序的(没有锁,没有礼让的情况下)。
创建方式:
- 继承
Thread
类,调用start方法来实现多线程的调用,虽然run
方法实际上也是调用的run
方法,但是如果直接调用run
方法并不会多运行一个线程。 - 实现
Runnable
接口,调用start
方法来多线程,相较于继承类,实现接口可以多实现。 - 实现
Callable
接口,重写call
方法来多线程,可以自定义返回值。
线程的一些方法和状态:
yield
-线程礼让:让当前运行的线程礼让其它的线程先运行,但是不会礼让低优先级的线程。join
-线程强制执行:当调用jion
方法的线程执行完毕之后,再执行其它的线程。sleep
-线程休眠:让线程指定时间过后再运行,不会释放锁。stop
和destory
-停止线程:方法被废弃,推荐让线程自己停下来。setPriority
-设置线程优先级:取值为1-10,值越大优先级越高。
线程同步:用来处理并发, 多个线程访问同一个方法时,需要等待前面的线程执行完毕,后面的线程才会执行进去。
锁:
使用synchronized
声明代码块,或者是在方法上添加。
Lock
锁,调用lock
对象的lock
方法加锁,调用unlock
释放锁。
二者比较:
Synchornnized
是内置的Java关键字,Lock
是一个关键字;Lock
可以判断锁的状态;Lock
手动的释放锁;- 如果一个线程阻塞,使用
Synchornized
会一直等待,Lock
会尝试使用tryLock
方法去尝试获取。 synchornized
是公平的,Lock
可以自己设置公共或者不公平。synchornized
适合少量的代码同步问题,Lock
适合大量的代码同步问题。
关于锁的问题建议直接看https://blog.csdn.net/weixin_44289860/article/details/107589088
锁的一些问题:
- 没有锁的方法与有锁的方法的调用无关;
- 与锁的休眠时间无关;
synchornized
锁的是调用方法的对象,也就是说如果一个对象调用两个方法,先拿到锁(先被调用就先拿到锁)的就先执行。- 不同的对象调用同步方法,不受对方锁的影响。
- 加了static标识符的方法锁的是全局唯一的class对象,多个对象也是同一把锁。
- 如果同时存在静态同步方法和普通同步方法,这个两个方法的锁的对象是不同的,所以不需要等待对方释放锁。
死锁:两个或者两个以上线程等待对方释放锁,就会形成死循环,程序一直卡在这。
线程协作:
多个线程共享一个资源,A线程使用完毕通知B线程,B完毕再通知A,依次循环。最典型的就是生产者和消费者模式。
解决方法:
7. 灯管法:都放到缓冲区中,不直接的进行通信协作。
8. 信号灯法。
9. 线程池:将创建好的线程放到池中,用时取,不用时退回。
10. JUC方法:lock相较于synchornized 的notifyAll方法变成了Condition对象的singalAll方法;wait变成了await方法。除去上述的区别外Condition还新增了对线程执行顺序控制的方法,对线程的精准通知唤醒。
juc
juc是java.util.concurrent
下面的atomic
和locks
包的简称。也是多线程的一种。一般用来解决并发编程的问题。
并发与并行:并发:多个线程操作同一个资源,并行:多个线程同时执行。
List、Set、Map
在并发下都是不安全的。
解决方法:
- list:
List<String> lists = new Vector<>();
或者List<String> lists = Collections.synchronizedList(new ArrayList<>());
再或者List<String> lists = new CopyOnWriteArrayList<>();
最好使用第三个,实现原理是在读写分离,在写入时避免覆盖而造成的数据问题;相较于Vector而言它的add方法并不是使用的synchornized锁同步的方法,效率高。 - set:
Set <String > strings = Collections.synchronizedSet(new HashSet<>());
或者Set<String > set = new CopyOnWriteArraySet<>();
- map:
Map<String,Sting> map = new ConncurrnetHashMap<>();
Callable
接口的补充:
常用的辅助类:
CountDownLatch
:类似于减法计数器,设定一个总数,每执行一个线程-1;当普通线程数量归零时执行指定的线程。有就是说等待一组线程执行完毕之后才会同时的去执行下面的代码。CyclicBarrier
类似与一个加法计数器,当线程增加到达指定的值时执行线程。当一组线程执行到某个状态时就可以继续执行,不用管其它的线程。Semaphore
类似于一个通行证,用来指定最多运行多少个线程, 每当一个线程运行完毕时,正在等待的线程再运行。一般用来限流和多个线程共享资源。
读写锁:
如果写入的过程中其它的线程也执行写入,当前写入线程就会变慢,所以使用读写锁:读可以被多个线程同时读,写的时候只能由一个线程写。独占锁:只有一个线程能够使用;共享锁:多个线程共享锁。读锁是共享锁,写锁是独占锁。
阻塞队列:
线程池补充:
三大方法:
ExecutorService service = Executors.newSingleThreadExecutor();
得到一个线程池;ExecutorService service = Executors.newFixedThreadPool(5);
得到固定线程数量的线程池;ExecutorService service = Executors.newCachedThreadPool();
根据实际情况来设定线程池的数量。
自定义线程池:
使用ThreadPoolExecutor
来创建线程。
拒绝策略:
AbortPolicy
:默认的拒绝策略,当线程数量超过最大的承受范围时会抛出异常。CallerRunsPolicy
:当超过线程池的最大承受范围时,由进入时的线程执行。DiscardPolicy
:队列满了会丢掉任务但是不会抛出异常。DiscardOldestPolicy
:队列满了尝试去和第一个调用的线程竞争,也不会去抛出异常。
Fockjoin:
分支合并:并行执行任务来提高效率。把大任务划分成小任务来执行,再将小任务合并为最终结果。
工作窃取:不让执行完毕任务的线程等待,去帮助其它的线程执行任务。
异步回调:
类似于前端的ajax,再Java中执行异步回调。
JMM:
Java内存模型
约定:
- 线程解锁前,必须把共享变量立刻刷新回主内存;
- 线程加锁前,必须把读取线程主内存中的最新值传到工作内存中;
- 必须保证加锁和减锁是同一把锁。
Vloatile:
Java虚拟机提供轻量级的同步机制。
- 保证可见性:如果工作内存无法及时的获得主内存中已经修改了的值,那么就不会及时的做出决定。
- 不保证原子性:
- 禁止指令重排:编译器优化会重排代码执行,指令并行也可能会重排,内存系统也会重排。处理器在执行指令重排的时候会考虑数据之间的依赖性。但是在多线程共同执行时,程序就不一定会出现应有的数值。添加volatile可以避免指令重排。原理:内存屏障:保证特定的操作的执行顺序。可以保证某些变量的内存可见性。在单例模式使用的最多。
原子性:不可分割。线程A在执行任务时,不能被打扰,要么同时成功要么同时失败。
如何保证原子性:
lock&synchornized
加锁可以保证原子性,但是消耗的资源比较大。
使用原子类->atomic