进程和线程都是CPU调用任务的概念。
进程:系统是系统分配资源的最小单位,进程包含1到多的线程,进程内的线程,可以有空闲的内存。
线程:系统调度任务的最小单位。
CPU是按进度顺序执行的,并发本质吧上是串行执行的。由于操作系统任务调度器的存在,使得多线程以分配CPU时间片的方式,实现并发执行。
并发:多线程轮流使用CPU
并行:多线程在多CPU的加持下,同时执行。
使用new Thread类
//创建线程 Thread thread = new Thread(){ @Override public void run() { super.run(); } }; //启动线程 thread.start();
使用 Runnable 配合 Thread
public class test2 { public static void main(String[] args) { //新建Runable的实现类 Runnable runnable = new myrunable(); //创建线程 Thread thread = new Thread(new myrunable(),"runable线程"); //启动线程 thread.start(); } } //新建实现Runable接口的类 class myrunable implements Runnable{ @Override public void run() { System.out.println("我是Runable方法创建"); } }
使用Callable创建,Callable可以有返回值,使用FuturnTask类接受返回值
public class Test3 { public static void main(String[] args) throws ExecutionException, InterruptedException { //创建Callable的实现类 Callable callable = new MyCallable(); //创建FuturnTask对象,并并绑定Callable对象,用来创建线程和接受返回值 FutureTask futureTask = new FutureTask(callable); //创建线程 Thread thread = new Thread(futureTask,"callable线程"); //启动线程 thread.start(); //获取线程完成后的返回值 futureTask.get(); } } //新建实现Callable的类 class MyCallable implements Callable{ @Override public Object call() throws Exception { System.out.println("我是callable创建"); return new Object(); } }
创建:虚拟机加载程序,创建了线程
就绪:虚拟机给线程分配好了资源,准备被cpu使用
运行:线程获得CPU时间片,在执行中
结束:线程zhongz
阻塞:
java中的用来上锁的是对象,java对象创建有mark word信息,在mark文件中标记,当前那个线程在使用该对象,从而实现上锁
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向 Monitor 对象的地址
Java锁具体可分为悲观锁/乐观锁、自旋锁/适应性自旋锁、偏向锁、轻量级锁/重量级锁、公平锁和非公平锁、可重入锁/非可重入锁、共享锁/排他锁
具体划分如下:
悲观锁:认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
乐观锁:认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据(可以使用版本号机制和CAS算法实现)。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)
应用场景
乐观锁的缺点
ABA问题
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题, 它可以通过控制变量值的版本来保证 CAS 的正确性。 大部分情况下 ABA 问题不会影响程序并发的正确性, 如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
自旋时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用, 第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源, 延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation) 而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
只能保证一个共享变量的原子操作 CAS只对单个共享变量有效,当操作涉及跨多个共享变量时CAS无效。 但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性, 可以把多个变量封装成对象里来进行 CAS 操作. 所以我们可以使用锁或者利用AtomicReference类把多个共享变量封装成一个共享变量来操作。
阻塞或者唤醒一个JAVA的线程需要操作系统切换CPU状态来完成,这种状态的转换需要耗费处理器时间。如果同步代码块中的内容过于简单,很可能导致状态转换消耗的时间比用户代码执行的时间还要长。所以在短暂的等待之后就可以继续进行的线程,为了让线程等待一下,需要让线程进行自旋,在自旋完成之后,前面锁定了同步资源的线程已经释放了锁,那么当前线程就可以不需要阻塞便直接获取同步资源,从而避免了线程切换的开销。这就是自旋锁。
自旋锁的优缺点
在资源被其它线程锁住是,不进行阻塞,而是自旋等待资源被释放。
优点: 需要同步代码块的执行时间很短,避免了CPU切换线程造成的性能和时间的浪费。
缺点: 需要同步的代码执行时间太长,线程自旋造成CPU空转等待时间太长,浪费夏普性能
适应性自旋锁
自适应意味着自旋的时间不在是固定,而是根据前次自旋的时间和有锁的线程状态来确定。如果自旋的线程刚成功获得锁,并且线程正在运行。虚拟认为获得锁的概率极大,允许自旋更长的时间。反之,自旋获得锁的成功很少,虚拟机避免自旋,直接阻塞线程。
当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
概念:是一种池话思路,通过重复利用已经创建的线程,减小线程创建和销毁带来的开销。使用线程时不需要创建,提高了响应速度。合理的管理线程资源,提高系统稳定性。
//使用ThreadPoolExcutor创建线程池 ExecutorService service = new ThreadPoolExecutor( 20, //corePoolSize : 核心线程数 30, //maximumPoolSize:最大线程数 2000, //keepAliveTime:线程存活时间 TimeUnit.MILLISECONDS, //unit:线程存活时间单位 new ArrayBlockingQueue<Runnable>(10),//workQueue:任务队列 new ThreadFactory() { //threadFactory:线程工厂 @Override public Thread newThread(Runnable r) { return null; } }, new ThreadPoolExecutor.AbortPolicy() //handler:拒绝策略 ); //给线程池提交任务,以Runable的w为任务参数 threadPool.execute(new Runnable() { @Override public void run() { } }); //关闭线程词 threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程 threadPool.shutdownNow();// 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
ThreadPoolEXcutor构造方法的几个参数:
任务队列(workQueue)
任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。但 Java 已经为我们提供了 7 种阻塞队列的实现:
线程工厂(threadFactory)
线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂:
new ThreadFactory() { //threadFactory:线程工厂 @Override public Thread newThread(Runnable r) { return null; } }
拒绝策略(handler)
当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略:
AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
CallerRunsPolicy:由调用线程处理该任务。
DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
new ThreadPoolExecutor.AbortPolicy(); //丢弃任务并抛出 RejectedExecutionException 异常。 new ThreadPoolExecutor.CallerRunsPolicy(); //由调用线程处理该任务。 new ThreadPoolExecutor.DiscardPolicy(); //丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。 new ThreadPoolExecutor.DiscardOldestPolicy();//丢弃队列最早的未处理任务,然后重新尝试执行任务。
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
jmm在处理线程共享变量时,有两个抽象的概念,线程本地内存和主内存。线程共享变量时存在主内存中,每个线程的本地内存有一个主内存的拷贝数据。当线修改共享变量时,是先修改线程本地内存里的数据,在同步到主内存。同步存在一个时间差,多线程修改可能出现同步数据错误。Volatile关键字可以使主内存对线程内存可见,避免了同步时间导致的数据错误
volatile保证了可见性,没有保障原子性。
ReentrantLock继承了Lock接口,在内部类中定义了Sync类继承AQS类,Sync被NonfairSunc和faisSync继承。
Atomic:是java JUC包下的Atomic包,以CAS为核心,在多线程的环境下,避免线程阻塞,以自旋的方式等待锁空闲。自旋适合占用锁时间较短,避免CPU任务调度造成的资源浪费
objectFieldOffset()
方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外value是一个volatile
变量,在内存中可见,因此JVM可以保证任何时刻任何线程总能拿到该变量的最新值AtomicInteger
类主要利用CAS
(compare and swap
) + volatile
和 native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。AtomicInteger
AtomicLong
AtomicBoolean
*三种类的方法基本一样,下面就以AtomicInteger 为例说明常用方法
public final int set(); //设一个值 public final int get() //获取当前的值 public final int getAndSet(int newValue)//获取当前的值,并设置新的值 public final int getAndIncrement()//获取当前的值,并自增 public final int getAndDecrement() //获取当前的值,并自减 public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 boolean compareAndSet(int expect, int update) //如果当前值等于预期值,则以原子方式将该值设置为输入值(update) public final void lazySet(int newValue) //最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
三种类的方法基本一样,下面就以AtomicIntegerArray 为例说明常用方法
public final int get(int i) //获取 index=i 位置元素的值 public final int set(int i, int newValue) //为 index=i 位置元素设新值 public final int getAndSet(int i, int newValue) //返回 index=i 位置的当前的值,并将其设置为新值:newValue public final int getAndIncrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自增 public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减 public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值 boolean compareAndSet(int i, int expect, int update) //如果index=i 位置的值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update) public final void lazySet(int i, int newValue) //最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
AtomicReference:引用类型原子类,引用的是复合类的数据。
AtomicStampedReference:AtomicReference的扩展版,增加了一个参数stamp标记,这里是为了解决了AtomicInteger和AtomicLong的操作会出现ABA问题。
AtomicMarkableReference
:与AtomicStampedReference差不多,只不过第二个参数不是用的int作为标志,而用boolean类型做标记,具体用法看后面讲解。
解释:
AtomicReference类,方便对复合类型的数据进行原子操作
AtomicStampedReference类,为了避免,ABA问题,引入了一个新的整形参数Stamp。只有当数据和参数都相同是,才认为一直
AtomicMarkableReference,和Stamped基本一致,maekable参数类型为布尔型
//定义类对象数据 User user1 = new User("金厂长",35); User user2 = new User("木易阿婆",26); /*****引用Atomic类******/ //新建引用类对象 AtomicReference atomicReference = new AtomicReference(User.class); //设置值 atomicReference.set(user1); //获取值 atomicReference.get(); //比较值,期望值和对象值一直,则修改 atomicReference.compareAndSet(atomicReference.get(),user2); //输出值 System.out.println(atomicReference.get()); /*******引用带盖戳类************/ //新建类对象 AtomicStampedReference<User> atomicStampedReference = new AtomicStampedReference<>(user1,10); //期望值和对象值一样,包括Stamp一直,则修改 atomicStampedReference.compareAndSet(atomicStampedReference.getReference(),user2,atomicStampedReference.getStamp(),10); //获取值 System.out.println("数据值wei: " + atomicStampedReference.getReference()); System.out.println("Stamp值为: "+atomicStampedReference.getStamp()); /*****AtomicMarkableReference*****/ //和AtomicStampedReference基本一张,Markable类型为布尔值 AtomicMarkableReference<User> atomicMarkableReference = new AtomicMarkableReference<>(user1,false);
AtomicIntegerFieldUpdater:原子更新整形字段的更新器
AtomicLongFieldUpdater:原子更新长整形字段的更新器
AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器
解释:
用来修改对象的属性,可以修改的属性类型是固定的。
能被修改的属性 不能是私有的 且必须配volatile修饰
参考使用
User类只有两个属性 volatile int age 和 volatile String name
//定义类对象数据 User user1 = new User("金厂长",35); User user2 = new User("木易阿婆",26); //新建修改引用类型类和int类 AtomicReferenceFieldUpdater<User,java.lang.String> arfu = AtomicReferenceFieldUpdater.newUpdater(User.class,java.lang.String.class,"name"); AtomicIntegerFieldUpdater<User> aifu = AtomicIntegerFieldUpdater.newUpdater(User.class,"age"); //修改数据 arfu.getAndSet(user1,"王老板"); aifu.addAndGet(user2,5); System.out.println(user1); System.out.println(user2);
理解:多线程工作,有产生数据,有消费数据,生产者消费者之间通过共享内存交换数据。
注意:
设计思路:
以数据词为核心,对外提供两个结构,消费和生产,为确保线程安全,生产消费共用一把锁。
这样设计便于理解阻塞队列的原理
没有使用wait() 和notify()方法。
使用了Locksupper.park()和Locksupper.unpark()。暂停继续线程功能
public class DataResprt { public static void main(String[] args) { //创建数据仓库对象 DataResprt dataResprt = new DataResprt(50); //创建生产消费线程池 ExecutorService productionServicer = Executors.newFixedThreadPool(10); ExecutorService consumerServicer = Executors.newFixedThreadPool(10); for (int i=0;i<10;i++){ productionServicer.execute(()->{ while (true){ try { TimeUnit.MILLISECONDS.sleep(1000); dataResprt.production(); } catch (InterruptedException e) { e.printStackTrace(); } } }); consumerServicer.execute(()->{ while (true){ try { TimeUnit.MILLISECONDS.sleep(500); dataResprt.comsumer(); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } //数据池大小 private int inventory=0; private final int MAX = 100; //构造函数 DataResprt(int inventory){ this.inventory =inventory; } //引入公平锁 ReentrantLock lock = new ReentrantLock(true); Condition condition = lock.newCondition(); //消费 消费 线程组 private List<Thread> consumerThreads = new ArrayList<>(); private List<Thread> productionThreads = new ArrayList<>(); //生产线程组 //消费接口 /** * 消费成功返回turn,否则falsh * **/ public Boolean comsumer() throws InterruptedException { //上锁 lock.lock(); if (this.inventory>0){ //库存大于0 //业务代码 this.inventory--; System.out.println(Thread.currentThread().getName() + "消费者:消费了一个数据,库存:"+this.inventory); //唤醒消费线程 if (consumerThreads.size()>0){ for(Thread thread:consumerThreads){ LockSupport.unpark(thread); } consumerThreads=new ArrayList<>(); } }else { //库存空 //添加到消费者线程组 consumerThreads.add(Thread.currentThread()); //阻塞当前线程, //condition.await(); //还是暂停当前线程吧,先释放锁 System.out.println(Thread.currentThread().getName() + "消费者:库存为空,任务暂停,释放锁。"); lock.unlock(); //释放锁 LockSupport.park(); //暂停线程 //线程继续后返回 return false; } //释放锁 lock.unlock(); return true; } //消费线程接口 /*** * 生产成功返回true,否则falsh */ public boolean production() throws InterruptedException { lock.lock(); if (MAX>this.inventory){//库存没满 //业务代码 this.inventory+=1; System.out.println(Thread.currentThread().getName() + "生产者:生产了一个数据,库存:"+this.inventory); //唤醒生产者 if (productionThreads.size()>0){ for(Thread thread:productionThreads){ LockSupport.unpark(thread); } consumerThreads=new ArrayList<>(); } }else{ //保存早生产组 productionThreads.add(Thread.currentThread()); //暂停生产者 System.out.println(Thread.currentThread().getName() + "生产者:库存满了,任务暂停,释放锁。"); lock.unlock(); //释放锁 LockSupport.park(); //暂停线程 //线程继续后返回 return false; } lock.unlock(); return true; } }
理解:一个只能能创建出一个对象,保证类的方法使用权只能有一个对象。
设计模式:
将构造方法私有化,外部不能调用。
预留一个对象的存储空间
获取时在创建对象
考虑多线程问题,getinstance()方法加锁,线程锁必须只能一个,使用类上锁
java能实现单例模式,受益于java对象的赋值是引址引用,获得的实例实际只是地址,并非复制
class MySingle{ //预留唯一对象存储空间 static volatile private MySingle mySingle = null; //私有化构造方法 private MySingle(){ System.out.println("我是MySingle唯一的构造方法,你看不到我第二次"); } //提供一个获得实例的结构 synchronized static public MySingle getMySingle(){ if (mySingle==null){ mySingle = new MySingle(); } return mySingle; } }