1 程序 进程 线程
-
程序: 一组静态的代码
-
进程: 一个处于运行状态的程序 。 进程 = 执行内存 + 线程
每当启动一个进程时,至少会有一个线程,称为:主线程
-
线程:程序运行的过程中,真正用来执行程序功能的那个单元
每当一个进程开启,都会产生一个主线程
可以根据设计需求,由主线程产生更多的子线程,共同完成程序的执行过程。
多线程的目的就是为了提高执行效率
2 创建线程
-
创建线程类 , 暂时2种方式
-
自定义线程类,实现Runnable接口,重写run方法 .
实现Runnable接口的类对象,允许被多线程单独运行。
-
自定义线程类,继承Thread父类,重写run方法
-
-
创建线程对象
-
new Thread()
-
class A extends Thread{public void run(){}
}class B implements Runnable{public void run(){}
}Thread a = new A();B b = new B();
Thread tb = new Thread(b);
关于run方法
-
run方法就是线程启动后,自动调用的方法。 就如同main方法是java程序启动后,自动调用的方法
-
我们在编码中,不能主动的调用run方法。一旦主动调用run方法,就不属于多线程操作
-
main方法是由主线程自动调用的, run方法就有子线程自动调用
-
子线程的启动需要一定的条件。
3 启动线程
-
线程对象在执行时,会自动调用run方法
-
如果(手动)主动的调用run方法,那不属于线程操作,只属于简单的面向对象操作。
-
从计算机底层来讲,真正能够执行代码指令的是CPU
-
当线程对象执行并能够处理代码指令时,也就说明线程对象获得了CPU(操作系统)
-
Java支持的抢占式获得CPU
-
线程抢到cpu会执行,执行一会就回释放cpu,再重新争抢。
-
所以多线程的执行顺序是不确定。
-
线程创建之后,必须启动才能开始争抢CPU
-
这里我们所谓的启动线程,不是执行线程,是让线程开始争抢cpu
-
如何启动线程呢?
-
调用
thread.start()
狭义多线程 和 广义多线程
-
有多个线程对象,但只有1个cpu 。 称为广义多线程
-
有多个线程对象,有多个cpu(处理器数量)
4 线程优先级
-
线程的执行过程是需要抢占CPU的
-
可以通过设置线程的优先级来提高线程抢占CPU的几率
-
调用
a1.setPriority(1);
设置优先级优先级的值1 - 10
所有新创建的线程,默认的优先级都是5
5 精灵线程
-
也叫守护线程,守护的是主线程
-
所有新创建的线程,称为用户线程
如果主线程执行完毕,但用户线程没有执行完毕,主线程会等待用户线程执行完后在关闭程序
-
守护线程是用来守护主线程的,只要主线程执行完毕,所有的守护线程立刻结束,关闭程序
-
调用
a.setDaemon(true);
设置线程是否为守护线程
7 线程同步
-
当多线程执行,对于一个共享变量进行操作时,会产生一些安全问题,称为同步问题。
-
线程同步就是在多线程访问共享变量时,通过一定的机制,使得线程可以按照一定的顺序,每次只能有一个线程访问共享变量。
-
针对于上述买票程序中,多个窗口卖出相同票的原因分析
-
每卖出一张表,都会count--
-
编码时这是一行代码,但jvm执行时,这可能是多行指令
-
从count中获取数据(放到操作数栈)
-
实现count-1
-
-
多个线程同时获取count参数,获取的就是同一个数值
-
同时会执行多次count--,从会发生,当下一个线程获取count值时不再连续了
-
-
如何解决线程同步的问题呢? 可以给程序加锁
-
加锁的特点,就是从进入加锁的代码,到执行完毕离开的这段过程中,只允许一个线程进入。
-
加锁可以实现部分代码的串行化
-
如何给代码片段加锁?有两种方式
-
悲观锁,必须要上锁,有具体的上锁操作
悲观锁有两种,使用synchronized关键字,使用Lock对象
-
乐观锁,不是真正意义上的锁,没有具体的锁操作,但有一个数据状态
每次操作数据前,会获得数据状态。在操作数据时,如果发现状态发生了变化,
基于新状态重新操作 JDK针对于数字计算,提供了原子类,底层使用的是CAS + 自旋机制
-
7.1 synchronized锁的使用
-
该关键字可以给方法上锁,也可以给代码段上锁
-
当线程调用同步方法或同步代码段时,就会获得一个对象锁
每一个对象创建时,都会有一个对象锁,本质是一个监视器Mointer
-
当synchronized关键字修饰的是方法时,调用该同步方法获得的是该方法所属对象的对象锁
而不是调用这个方法的线程对象的对象锁
当线程争抢对象锁时,如果对象所以被占用,线程将处于等待状态
当另一个线程执行完毕,释放了占有的锁, 这些等待的线程才能继续执行。
-
线程进入等待状态,本质就是加入了一个同步等待队列(是系统级别的)
-
同步方法
public synchronized void t1(){for(int i=1;i<=10;i++){System.out.println(Thread.currentThread().getName()+"="+i);}
}
同步代码段
public void t2(){int no = (int)(Math.random()*10);if(no < 5){System.out.println("byebye");System.out.println("byebye");System.out.println("byebye");System.out.println("byebye");System.out.println("byebye");return ;}synchronized ("buka") {for (int i = 1; i <= 10; i++) {System.out.println(Thread.currentThread().getName() + "=" + i);}}}
-
-
同步代码段所需要的锁,是通过传参指定的。只要保证传递对象是唯一的即可。
-
静态同步方法获得的是哪个对象的对象锁呢?
-
获得是类模板对象的对象锁 Class对象的对象锁
-
Class类结成某一个类的类模板表示, 一个类必然只有一个类模板
7.2 Lock锁的使用
-
synchronized锁在使用时存在着一定的不足
-
所以在jdk1.5时,提供了一个JUC java 并发 工具包。里面包含了许多与多线程操作相关的工具类
-
其中就包含了Lock系列
-
Lock本身是一个接口,我们实际应用时使用其对应的子类
ReentrantLock
-
-
Lock的使用过程
-
创建锁对象。 如果多个线程需要争抢一把锁,就创建一个锁对象。 需要多个锁,就创建多个锁对象
-
调用lock对象的
lock()
尝试争抢锁。 如果抢不到锁,线程进入等待状态。有一个从在方法
tryLock(10,TimeUnit.SECONDS)
尝试在指定的时间内获得锁。返回boolean -
调用lock对象的
unlock()
,释放锁。 其他等待锁的线程才能继续争抢。
-
static final Lock lock = new ReentrantLock();
public static void t1(){lock.lock();for (int i = 1; i <= 10; i++) {System.out.println(Thread.currentThread().getName() + " : " + i);}lock.unlock();
}
在使用lock锁,建议将获得锁的代码写在try中,将释放锁的代码写在finally中
确保无论操作是否成功还是是否,都能释放锁。
try{lock.lock();...
}finally{lock.unlock();
}
lock锁底层机制
-
底层使用的是CAS + AQS
-
在lock底层有一个计数器,记录锁被获取的状态,起初为0 , 当被抢占的时候变为1
也就是说,当我们调用lock.lock()方法,就是将状态从0改为1的过程
当我们调用lock,unlock()方法时,就是将状态从1改为0的过程
当我们调用lock.lock方法时,如果发现状态是1,表示锁被占用,当前线程进入等待状态
-
当多个线程同时访问lock.lock()方法时,就想将状态从0改为1
每个线程都尝试着将状态从0改为1. 假设a线程最先完成状态改变,a线程获得了锁
此时b线程也尝试将0改为1,先获得原始值0,将0改为1,再将改好的1替换原始值0(赋值)
在替换前,会拿着之前的原始值与现在变量里的值进行比较,看看是否发生了变化
如果没有变化,说明这个过程中没有其他线程访问资源,将其改为1获得锁。
如结发生了变化,说明这个过程中被其他线程捷足先登了,其他的线程获得锁,当前线程等待
我们称这个过程为 CAS (compare and set)
-
当一个线程获得锁时,发现锁已经被占用了,当前这个线程就会处于等待状态
实际上,并不是线程对象有一个状态码,改为等待状态的值。
实际上是将需要等待的线程,存入了一个集合,并使其进入最终等待状态(jvm级别的等待状态)
当最开始线程执行完毕释放锁后,就会从这个集合中取出最开始的那个线程继续执行
这个集合我们就称为 AQS (抽象的)队列同步器
-
7.3 乐观锁
-
乐观锁不是一种真正存在的锁,一种机制
-
底层使用的是 CAS + 自旋 应用组合
//count++; //一假设线程1获得count值为0并记录为原始值 //二接着会基于0实现++操作,变成1 //三接着会尝试将1赋值回count变量中 // 1. 根据之前记录的原始值0 与此时此刻count变量中的值比较 // 2. 如果相等,说明这个过程中没有其他线程修改count值,表示这段时间当前线程占有这变量 // 完成赋值 // 3. 如果不相等。比如count已经=2了,说明这个过程中,有其他线程使用过这个变量 // 如果继续赋值,影响了其他线程的操作结果,所以不能赋值,此次操作失败 // 重新获取现在的最新的值,记录为原始值2,重复一操作 ,直到能够完成赋值位置 // 我们称这个重复过程为:自旋
-
乐观锁的使用更具有局限性, 适用于数字变量的计算,一般多是 ++ 和 --
-
JDK中提供了一个原子类 AtomicIntger,该类对象中提供了++和--的计算方法
当通过该类对象提供的++和--的方法计算时,可以确保线程同步。
AtomicInteger的常用方法
static AtomicInteger count = new AtomicInteger(0);count.get(); //获得变量值(不是同步的)
count.set(value);//给变量赋值(不是同步的)count.getAndIncrement(); //等价于count++
count.incrementAndGet(); //等价于++count
count.getAndDecrement(); //等价于count--
count.decrementAndGet(); //等价于--count
count.getAndAdd(v); //等价于count += v
注意1:CAS的过程会不会出现同步问题
-
不会出现同步问题,CAS底层是基于系统实现的
-
实现时最终也会上锁。 也就是当一个线程在CAS时,其他线程处于等待状态
-
虽然最终都上锁了,但与被锁的颗粒度不同。
注意2:如何解决CAS过程中的ABA问题
-
CAS特点是,在set设置新值之前,先用原始值与当前变量中的值做比较
-
相等说明这个过程中没有其他线程操作变量,也就相当于当前线程占有变量
-
不相等说明这个过程中有其他线程操作变量,也就是这个变量其他线程占有,我需要重新获得
-
现在的问题是,在比较时原始值与当前变量中的值相等,就能说明这个过程中变量没有被其他线程操作么? 不能
-
因为有可能另一个线程将变量中的值,从A改成了B又改回成了A
-
ABA问题如何解决呢? 可以为数据增加一个版本号,只要改变过,版本号就+1
-
JDK提供了一个可以解决ABA问题的原子类
public static void main(String[] args) throws InterruptedException {AtomicStampedReference<Integer> count = new AtomicStampedReference<>(0,1);Thread t1 = new Thread(()->{Integer oldValue = count.getReference();int oldVerion = count.getStamp();try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}boolean f = count.compareAndSet(oldValue,oldValue+1,oldVerion,oldVerion+1);System.out.println(Thread.currentThread().getName()+" : " + f);});t1.start();Thread.sleep(100);Thread t2 = new Thread(()->{Integer oldValue = count.getReference();int oldVerion = count.getStamp();boolean f = count.compareAndSet(oldValue,oldValue+1,oldVerion,oldVerion+1);System.out.println(Thread.currentThread().getName()+" : " + f);oldValue = count.getReference();oldVerion = count.getStamp();f = count.compareAndSet(oldValue,oldValue-1,oldVerion,oldVerion+1);System.out.println(Thread.currentThread().getName()+" : " + f);});t2.start();
}