1.创建线程方式
-
继承Thread类,重写run()方法,调用start方法开启线程
-
实现Runnable接口
-
实现Callable接口(可获取返回值)
-
Executors.newCachedThreadPool() 通过线程池创建
阿里推荐创建线程池的方式: new ThreadPoolExecutor()
//1.继承Thread类,重写run()方法,调用start方法开启线程
public class ThreadTest extends Thread {
@Override
public void run() {
for (int i = 0; i < 2010; i++) {
System.out.println("打豆豆");
}
}
public static void main(String[] args) {
new ThreadTest().start();
}
}
//2.实现Runnable接口
public class RunnableTest implements Runnable {
@Override
public void run() {
for (int i = 0; i < 2010; i++) {
System.out.println("打豆豆");
}
}
public static void main(String[] args) {
new Thread( new RunnableTest()).start();
//也可用lambda表达式
new Thread(()->{System.out.println("打豆豆");}).start();
}
}
//3.实现Callable接口
public class CallableTest implements Callable<Boolean> {
@Override
public Boolean call() {
System.out.println("11111");
return true;
}
public static void main(String[] args) {
CallableTest p1 = new CallableTest();
//1.创建线程池
ExecutorService serve = Executors.newFixedThreadPool(3);
//阿里推荐创建线程池的方式
threadPool = new ThreadPoolExecutor(8, 16, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(32), new ThreadPoolExecutor.CallerRunsPolicy());
//2.提交执行
Future<Boolean> r1 = serve.submit(p1);
//3.获取结果
Boolean res1 = r1.get();
//4.关闭服务
serve.shutdownNow();
}
}
注意:创建线程调用方式
1.new T1().run(); 仅是执行run()方法,并不开启一个线程
2.new T1().start() 开启一个线程,执行run()方法
2.线程方法
- 线程睡眠: Thread.sleep(1000)
- Thread.sleep(0) :触发一次cpu竞争,防止在cpu抢占式算法中,一个线程长时间占用cpu资源
- 线程礼让: Thread.yeild() 重新进入等待队列(让出一线cpu,返回就绪状态,同时也会参加cpu的竞争)
- 线程插队: t1.join() : 需要在t2(其他线程)执行的时候,t1插队,必须等t1执行完才执行t2(其他线程)
- 获取线程状态: t1.getState();
Ready和Running 合为一个 Runnable状态
3.Synchronized关键字(隐式定义,出了作用域自动释放)
1.同步代码块:(默认锁的对象为Obj),执行完代码块就会释放锁
synchronized(Obj){//Obj称为同步监视器,Obj可以锁任意对象,但是推荐使用共享资源作为同步监视器
...
}
2.同步方法 (默认锁的是方法的对象)
public synchronized void method(int args){
...
}
synchronized 方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程阻塞,方法一旦执行,就独占该锁,直到该方法返回才能释放锁,后面被诸塞i的线程才能获得这个锁,继续执行。
缺陷:若将一个大的方法申明为synchronized将会影响效率
同步方法的同步监视器是 this就是对象本身,或者的class(反射)
注意:
1.同一个类中:synchronized(this ){…} 与 public synchronized void method(){…} 锁的为同一个对象,两种表达等价
class T{
//方法1和2表达的效果一样
public void method1(){
synchronized(this){
System.out.println("000");
}
}
public synchronized void method2(){
System.out.println("000");
}
//synchronized 静态方法 锁的是class对象
public synchronized static void method(){
...
}
}
2.public synchronized static void method(){…} 锁的是 T.class
3.**脏读:写方法加锁,读方法不加锁,(读到未被修改的值)**解决:同时加锁,(但效率低)
4.synchronized:保证可见性,一致性,可重入锁 :
5.可重入锁:广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁 。某个线程已经获得某个锁,可以再次获取锁而不会出现死锁
6.程序产生异常时会自动释放锁
Synchronized锁升级
锁的4中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)
1.偏向锁:MarkWord记录线程ID,判断其他线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁 ; 如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。(多个线程争用,升级为自旋锁,自旋消耗cpu资源)
2.轻量级锁
自旋次数超过(10/100次)时升级为重量级锁。
3.重量级锁(向操作系统申请,进入等待队列,)
***注意:**为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁 可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
总结:
- 加锁代码执行时间短或线程数少, 用轻量级锁
- 加锁代码执行时间长或线程数多, 用重量级锁
synchronized(Object)
锁的对象不能是基础数据类型: 向String常量,Interger,Long
4.volatile 关键字
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
1.保证可见性(通过MESI 缓存一致性协议)
当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去,这个写会操作会导致其他线程中的volatile变量缓存无效。
线程之间不可见: 一个线程修改对copy到自己线程工作内存的共享变量,另一个线程无法看到共享变量的改变
2.禁止指令重排序(读屏障,写屏障,原语操作)
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2.它会强制将对缓存的修改操作立即写入主存;
3.如果是写操作,它会导致其他CPU中对应的缓存行无效。
3.不保证原子性
//1.双重检测实现单例
public class SingleInstance {
//加volatile 禁止指令重排,因为new SingleInstance() 并不是一个原子性的操作(1.分配内存(默认值) -> 2.为成员变量赋值 ->3.将值指向内存)
//在超高并发的情况下:第一个线程进行分配内存(此时对象已不为空),第二个线程判断该实例不为空,进行取值时取的是初始化的默认值,并不是第二步的真实值
public static volatile SingleInstance singleInstance;
private SingleInstance() {
}
public static SingleInstance getInstance(){
if(singleInstance==null){
synchronized (SingleInstance.class){
if(singleInstance==null){
singleInstance=new SingleInstance();
}
}
}
return singleInstance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(SingleInstance.getInstance());
}).start();
}
}
}
//2.通过枚举实现单例
class Resource{
}
public enum SingleInstanceEnum {
INSTANCE;
private Resource resource;
SingleInstanceEnum(){
resource=new Resource();
}
public Resource getInstance(){
return resource;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(SingleInstanceEnum.INSTANCE.getInstance());
}).start();
}
}
}
5.CAS(compare and set/swap)无锁优化,自旋
CPU 原语支持:不可被打断
CAS(Expected, NewValue) 循环:当 当前值 刚好为期望的值(Expected)(即没有其他线程进来改过值)时, 值改为新值(NewValue) ,若不为期望值,就表示有线程进来改过值,需要重新读当前值,重新设期望值
- Expected :期望的值
- NewValue : 要改为的值
ABA问题:cas会导致ABA问题(一个线程将原值A修改为B值,然后又将B值修改为A值,但对于另一个线程来说并不知道他发生了改变)
基本数据类型:不影响
引用类型: 有影响(通俗的解释为,你的女朋友跟你复合,你女朋友中间经历了n个男人)
解决办法: 加一个版本号vision,时间戳
Unsafe类:直接操作jvm内存(类似于c++的指针)
所有 atomic 类(如:AtomicInteger)都是通过unsafe类里的 compareAndSwap 来操作的
高并发操作同一个数的方案:
- long类型加锁
- atomicLong 类
- LongAdder (线程数多时有优势,分段锁)
6.lock锁(手动开启和关闭)
jdk1.5开始的
使用Lock锁,jvm将花费较少的时间来调度线程,性能更好,并且具有良好的扩展性(提供更多的子类)
1.java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得锁
可重入锁:锁的对象为同一个时,无需再次获取锁
2.**ReentrantLock(可重入锁)**类实现了Lock,他拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,常用ReentrantLock,可以显式加锁,释放锁
使用优先级: lock>同步代码块>同步方法
lock优点:
- lock.tryLock(5,TimeUnit.SECONDS) 尝试在5s内获取锁
// lock的使用方式
private final ReentrantLock lock =new ReentrantLock(); //参数可指定公平/非公平锁,默认为非公平锁
try{
lock.lock();
........
}finally {
lock.unlock();
}
JUC 辅助类
1.CountDownLatch :减法计数器 用于等待几个线程结束,countDown为0时唤醒
import java.util.concurrent.CountDownLatch;
//闭锁,用于等待时间
//1.总数为6的计数器
public class CountDownLatchTest {
public static void main(String[] args) {
//1.创建一个总数为6的计数器
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"走了");
//2.计数器减一
countDownLatch.countDown();
},String.valueOf(i)).start();
}
try {
//等待计数器归零时,唤醒countDownLatch.await(),继续向下执行
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束");
}
}
2.CyclicBarrier 加法计数器(等待线程数满了调用指定动作(BarrierAction),不满就等待)
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
//栅栏:用于等待其他线程(全部完成才会向下执行)
//2.加法计数器
public class CyclicBarrierTest {
public static void main(String[] args) {
//创建总数为7个的计数器
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("成功!!!");
});
for (int i = 0; i < 7; i++) {
//
final int temp=i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"现在是"+temp);
try {
//等待集齐7个线程时才会执行下一步
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
3.Semaphore 信号量 限流(限制多个线程 最多只能运行的数量)
例: 4个车道(多个线程进入),2个收费出口(最多只能允许两个线程运行)
import java.util.concurrent.Semaphore;
//3.信号量
//semaphore.acquire() 获得,如果已经满了,将会阻塞等待
//semaphore.release() 释放,将当前信号量+1,同时唤醒等待的线程
//作用,多个共享资源互斥的使用,并发限流,控制最大的线程数
public class SemaphoreTest {
public static void main(String[] args) {
//参数为线程数量,限流
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
//1.获取(Semaphore--),semaphore为0时阻塞
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到了车位");
//2.释放(Semaphore++)
semaphore.release();
System.out.println(Thread.currentThread().getName()+"离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
4.Phaser : 阶段操作,达到一个阶段后,才会继续往下执行
注册机制:与其他barrier不同的是,Phaser中的“注册的同步者(parties)”会随时间而变化,Phaser可以通过构造器初始化parties个数,也可以在Phaser运行期间随时加入(register)新的parties,以及在运行期间注销(deregister)parties。运行时可以随时加入、注销parties。
package com.example.demo.util;
import java.util.concurrent.Phaser;
public class PhaserTest {
public static void main(String[] args) {
WorkPhaser workPhaser = new WorkPhaser();
// 4个工人参与工作
for (int i = 0; i < 4; i++) {
// 注册
workPhaser.register();
// 启动线程
new Thread(new Worker(workPhaser), "work-" + (i + 1)).start();
}
}
}
/**
* 工作阶段器
*/
class WorkPhaser extends Phaser {
//所有线程满足条件时,自动调用 onAdvance()方法
@Override
protected boolean onAdvance(int phase, int registeredParties) {
switch (phase) {
case 0:
System.out.println("第一阶段工作完成,进入第二阶段>>>");
return false;
case 1:
System.out.println("第二阶段工作完成,进入第三阶段>>>");
return false;
case 2:
System.out.println("第三阶段工作完成,退出.");
return true;
default:
return true;
}
}
}
/**
* 工人
*/
class Worker implements Runnable {
private WorkPhaser workPhaser;
public Worker(WorkPhaser workPhaser) {
this.workPhaser = workPhaser;
}
@Override
public void run() {
String playerName = Thread.currentThread().getName();
System.out.println(playerName + " 工人完成了第一阶段的工作.");
// 到达阶段,等待进入下一阶段
workPhaser.arriveAndAwaitAdvance();
System.out.println(playerName + " 工人完成了第二阶段的工作.");
// 到达阶段,等待进入下一阶段
workPhaser.arriveAndAwaitAdvance();
System.out.println(playerName + " 工人完成了第三阶段的工作.");
// 到达阶段,等待进入下一阶段
workPhaser.arriveAndAwaitAdvance();
}
}
// workPhaser.arriveAndDeregister() //不用等,自己注销
5.ReadWriteLock 读写锁
读写时互斥,单独读时是共享锁(只读不加锁会出现脏读),单独写时为独占锁
package com.it.juc.readWriteLock;
import javax.swing.plaf.IconUIResource;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
public static void main(String[] args) {
MyCacheLock myCacheLock = new MyCacheLock();
for (int i = 0; i < 5; i++) {
final int temp =i;
new Thread(()->{
myCacheLock.put(temp+"",temp);
},"A").start();
}
for (int i = 0; i < 5; i++) {
final int temp =i;
new Thread(()->{
myCacheLock.get(temp+"");
},"B").start();
}
}
}
class MyCacheLock{
private volatile Map<String,Object> map=new HashMap<>();
//读写锁,更加细粒度的控制
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()+"写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
map.get(key);
System.out.println(Thread.currentThread().getName()+"读取完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
6.exchange
俩线程之间交换信息,第一个线程需要等待第二个线程的进入才会交换,等待过程阻塞,交换完继续执行。 (场景:交换装备)
package com.securitit.serialize.juc;
import java.util.concurrent.Exchanger;
public class ExchangerTester {
// Exchanger实例.
private static final Exchanger<String> exchanger = new Exchanger<String>();
public static void main(String[] args) {
// 模拟阻塞线程.
new Thread(() -> {
try {
String wares = "红烧肉";
System.out.println(Thread.currentThread().getName() + "商品方正在等待金钱方,使用货物兑换为金钱.");
Thread.sleep(2000);
String money = exchanger.exchange(wares);
System.out.println(Thread.currentThread().getName() + "商品方使用商品兑换了" + money);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}).start();
// 模拟阻塞线程.
new Thread(() -> {
try {
String money = "人民币";
System.out.println(Thread.currentThread().getName() + "金钱方正在等待商品方,使用金钱购买食物.");
Thread.sleep(4000);
String wares = exchanger.exchange(money);
System.out.println(Thread.currentThread().getName() + "金钱方使用金钱购买了" + wares);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}).start();
}
}
transient 关键字
transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程。
作用:
Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的
7.死锁(deadlock)
多个线程各自占用有限资源,并且互相等待其他线程占有的资源才能运行,而导致两个多多个线程都在等待对方释放资源,都停止执行的情形,某个同步代码块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”
死锁的4个必要条件:
1.互斥条件: 一个资源每次只能被一个进程使用
2.请求与保持条件:一个进程因请求资源而阻塞时,对以获得的资源保持不放
3.不剥夺条件:进程对以获得的资源,在未使用完之前,不能强行剥夺·
4.循环等待条件:若干进程之间形成一种头尾相交的循环等待资源的关系
8.线程通信:
9.线程池:
//线程池
public class ThreadPool {
public static void main(String[] args) {
//1.创建服务,创建线程池
ExecutorService service = Executors.newFixedThreadPool(5);
//2.执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//3.关闭连接
service.shutdownNow();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
10.生产者消费者模型:
package com.it.MulThread.produceConsumer;
//1.管程法: 利用缓冲区解决
public class PC {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
new Producer(synContainer).start();
new Consumer(synContainer).start();
}
}
class Chicken{
int id;
public Chicken(int id) {
this.id = id;
}
}
class Producer extends Thread{
SynContainer synContainer;
public Producer(SynContainer synContainer){
this.synContainer=synContainer;
}
//生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synContainer.push(new Chicken(i));
}
}
}
class Consumer extends Thread{
SynContainer synContainer;
public Consumer(SynContainer synContainer){
this.synContainer=synContainer;
}
//消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synContainer.pop();
}
}
}
class SynContainer{
//容器
Chicken[] chickens =new Chicken[10];
//容器计数器
int count=0;
//生产者放入产品
public synchronized void push(Chicken chicken){
//如果日期满了
if(count==chickens.length){
try {
//生产者等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//没有存满,就生产进容器
chickens[count++]=chicken;
System.out.println("生产了"+chicken.id+"只级");
//通知消费者消费
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop(){
//判断能否消费
if(count==0){
//等待生产者生产
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以消费
count--;
Chicken chicken = chickens[count];
System.out.println("消费了第"+chicken.id+"只级");
//吃完了,通知生产者生产
this.notifyAll();
return chicken;
}
}
问题:为什么ArrayList线程不安全?
首先说一下什么是线程不安全:线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。如图,List接口下面有两个实现,一个是ArrayList,另外一个是vector。从源码的角度来看,因为Vector的方法前加了,synchronized 关键字,也就是同步的意思,sun公司希望Vector是线程安全的,而希望arraylist是高效的,缺点就是另外的优点。说下原理(百度的,很好理解):一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:
- 在 Items[Size] 的位置存放此元素;
- 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
线程安全的list : CopyOnWriteArrayList