一、volatile关键字内存可见性
当程序运行时,JVM会为每一个执行任务的线程分配一个独立的缓存空间,用于提高效率。这个线程会先从主内存中拿到变量到自己的缓存中,然后将改变后的值提交到主内存,这是两个操作,如果在第一个操作后,第二个线程闯入,这个时候第二个线程从主内存中读取的变量就是未改变之前的变量,那么两个线程最后拿到的值便是不一样的,产生冲突。
产生此种问题是因为这个变量并不是都在主内存中操作的,要解决这个问题,便是在“共享变量” 定义的时候在之前添加一个 volatile 关键字。
图示:
捕获.PNG
二、原子变量-CAS算法
volatile关键字保证内存可见性,也就是说可以保证变量都在主内存中进行,但是不能保证原子性。
(一)那么什么是原子性问题?
举例:i++操作
i++操作实际上是“读-改-写”三个操作:
int temp = i;
i=i+1;
temp = i;
当我们运行程序:
int i = 10;
i=i++;
System.out.println(i); //这个时候输出打印的应该是 10
原因在于:此时 i++操作返回的是底层“读”的时候的 i ,而不是“写” 完后的 i
: 这就是原子性问题
(二)原子变量(解决原子性问题)
为了解决这个问题,jdk 1.5之后,在java.util.concurrent.atomic包下提供了常用的数据类型的原子变量(最近更新:不过atomic...类也有它的局限性,比如AtomicLong的实现方式是内部有个value 变量,当多线程并发自增,自减时,均通过CAS 指令从机器指令级别操作保证并发的原子性。唯一会制约AtomicLong高效的原因是高并发,高并发意味着CAS的失败几率更高, 重试次数更多,越多线程重试,CAS失败几率又越高,变成恶性循环,AtomicLong效率降低,Java8对此有了新的解决方案:LongAdder)。
它的底层实现是:
1、首先用volatile保证内存的可见性
2、然后用CAS算法保证数据原子性,CAS算法是硬件对于并发操作共享数据的支持,CAS包括三个操作:
①内存值(从主存中读取值)、
②预估值(读取旧值)、
③更新值(如果满足条件就替换主内存中的值)
只有当内存值==预估值的时候,内存值才能够等于更新值,否则将不作任何操作。
demo:
/**
* @Author : WJ
* @Date : 2018/11/18/018 12:26
* <p>
* 注释:
*/
public class Test2 {
public static void main(String [] a) throws InterruptedException {
AtomicDemo atomicDemo = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(atomicDemo).start();
}
}
}
class AtomicDemo implements Runnable {
//用volatile保证内存可见性:无法保证原子性
//private volatile int number = 0;
//使用原子变量解决原子性问题
private AtomicInteger number = new AtomicInteger();
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getAdd());
}
public int getAdd(){
//使用原子变量提供的相关API进行对原子变量数据的操作,API文档里面有详细介绍
//这里使用“从主内存中获取和自增”一起的方法返回number自增的值。
return number.getAndIncrement();
//return number++;
}
}
三、ConcurrentHashMap锁分段机制
Java 5.0 在 java.util.concurrent 包中提供了多种并发容器类来改进同步容器的性能。
ConcurrentHashMap 同步容器类是Java 5 增加的一个线程安全的哈希表。对与多线程的操作,介于 HashMap 与 Hashtable 之间。内部采用“锁分段”机制替代 Hashtable 的独占锁。进而提高性能。
此包还提供了设计用于多线程上下文中的 Collection 实现:
ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap,ConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。
JDK1.8以后ConcurrentHashMap由锁的分段机制变为CAS。
CopyOnWriteArrayList "写入并复制" 是个复合操作,当每次写入时,都会复制。添加操作比较多时效率较低。并发迭代操作多时,可以提高效率。
这里推荐一篇博客详细介绍了ConcurrentHashMap:
https://blog.csdn.net/yansong_8686/article/details/50664351
四、CountDownLacth 闭锁
闭锁是一个同步工具类,它是用来保证一组线程全部执行完成才能进行下一步操作的工具。就比如一组多线程,我们想要获取他们全部执行的时间,寻常操作时无法完成的,因为获取时间存在于主线程,随时可能拿到cpu的使用权利,所以这个工具类,就可以实现要全部线程执行完,才执行下一步。
闭锁状态包含一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示已经有一个事件已经发生了。而await方法等待计数器达到0,这表示所有需要等待的事件都已经发生。如果计数器的值非0,那么await会一直阻塞直到计数器为0,或者等待中的线程中断或者超时。
当然采用join方式也可以完成,但是效率是远远不及的。
demo:
/**
* @Author : WJ
* @Date : 2018/11/18/018 12:26
* <p>
* 注释:
*/
public class Test2 {
public static void main(String [] a) throws InterruptedException {
//闭锁工具类,设需要等待事件数完成的数量为 5
final CountDownLatch countDownLatch = new CountDownLatch(5);
DownLatchDemo downLatchDemo = new DownLatchDemo(countDownLatch);
//开始时间
long start = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
new Thread(downLatchDemo).start();
}
//等待上面的5个线程执行完成,也就是事件数为 0 后才执行await之后main线程的代码
countDownLatch.await();
//结束时间
long end = System.currentTimeMillis();
System.out.println("执行时间为:"+(end - start));
}
}
class DownLatchDemo implements Runnable {
private CountDownLatch latch;
DownLatchDemo(CountDownLatch latch){
this.latch = latch;
}
public void run() {
synchronized (this){
try{
//执行一个耗时的操作
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName());
}
}finally {
//每一次线程的调用,使闭锁操作事件数减 1
latch.countDown();
}
}
}
}
五、创建实现线程的方式
创建实现线程的方式有四种:
1、继承Thread接口
2、实现Runable接口
3、实现Callable接口
4、线程池
对于Callable接口:
1、需要实现Callable接口,这里可以实现泛型接口Callable<?>
2、重写call方法,call方法可以返回泛型接口里面的数据类型数据
3、然后在启动这个线程的时候需要FutureTask类的支持,当然这个类就是获取线程返回值的类
demo:
/**
* @Author : WJ
* @Date : 2018/11/18/018 12:26
* <p>
* 注释:
*/
public class Test2 {
public static void main(String [] a) throws InterruptedException, ExecutionException {
CallableDemo callableDemo = new CallableDemo();
//要启动实现Callable 接口的线程类 需要 FutureTask 类的支持,用于接收运算结果
FutureTask futureTask1 = new FutureTask(callableDemo);
//启动线程
new Thread(futureTask1).start();
//获取线程返回值
System.out.println(Thread.currentThread().getName()+"得到返回值:"+futureTask1.get());
}
}
//实现Callable泛型接口,也可不实现泛型,call返回的将是一个Object类型的数据
class CallableDemo implements Callable<Integer> {
private volatile int number;
//重写call方法
public synchronized Integer call() throws Exception {
number = number +1;
System.out.println(Thread.currentThread().getName()+"为:"+number);
return number;
}
}
六、java中实现同步的两种方式: syschronized 和 lock
syschronized 实现同步的方式分为:同步方法 和 同步代码块
lock 是一个接口 ,通过:
private Lock lock = new ReentrantLock();
//然后lock调用:
lock.lock();
//....同步代码
lock.unlock();
//获得锁和释放锁
还可以:
private Condition condition = lock.newCondition();
调用:
condition.await();
condition.signal();
condition.signalAll();
等待和唤醒单个或所有线程,与wait 和notify、notifyAll不同的是:可以实现多路分用,也就是说将多个线程拆分等待,可以唤醒某一个确定同步线程。
但是syschronized 可以自动的获取和释放锁,而lock则需要显示的获取和释放,释放锁lock.unlock(); 必须放在try ... finally 的finally里面执行。
当线程竞争较激烈的话,Lock 性能优于 syschronized 。两者取决于业务的需求。
七、读写锁ReadWriteLock
保证 :读读、读写不是互斥的,写写是互斥(事件不能同时发生)的。
/**
* @Author : WJ
* @Date : 2018/11/18/018 12:26
* <p>
* 注释:
*/
public class Test2 {
public static void main(String [] a) throws InterruptedException, ExecutionException {
final DemoClass demoClass = new DemoClass();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
public void run() {
demoClass.get();
}
}).start();
}
new Thread(new Runnable() {
public void run() {
double number = Math.random()*100;
demoClass.set((int)number);
}
}).start();
}
}
/**
* 读写锁实例
*/
class DemoClass {
private int number;
//创建读写锁实例
private ReadWriteLock lock = new ReentrantReadWriteLock();
//读
public void get() {
lock.readLock().lock();
try{
System.out.println("读--操作:number = "+ number);
}finally {
lock.readLock().unlock();
}
}
//写
public void set(int number){
lock.writeLock().lock();
try{
this.number = number;
System.out.println("写++操作:number = "+ number);
}finally {
lock.writeLock().unlock();
}
}
}
八、线程池
1、为什么要用线程池?
当我们想要多次启动同一线程时,每一次启动都有创建和销毁操作,这样对于高并发的情况是不利的,所以就有了线程池的概念。
2、什么是线程?
线程池底层是实现一个对列,这个对列里面存放着多个线程,这样就不要每次创建都要销毁,影响效率。
3、线程池核心接口:Executor (位于Java.util.concurrent包下)
4、线程池体系结构
java.util.concurrent.Excutor :负责线程的使用与调度的接口
|------ExecutorService 子接口:线程池主要接口
|-------------ThreadPoolExecutor :线程池的实现类
|-------------ScheduledExecutorService:子接口,负责线程的调度
|--------------------ScheduledThreadPoolExecutor:继承ThreadPoolExecutor ,实现 ScheduledExecutorService
5、工具类:Executors
|----ExecutorService newFixedThreadPool(); 创建固定大小的线程池
|----ExecutorService newCachedThreadPool(); 缓存线程池,线程池数量不确定,可以根据需要自动的更改数量
|----ExecutorService newSingleThreadPoolExecutor(); 创建固定大小的线程,可以延迟或定时的执行任务。
6、线程池的使用:
//实现Runable的线程类
final DemoClass demoClass = new DemoClass();
//创建固定大小为5的线程池
final ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
//为线程池中的线程分配任务
pool.submit(new Thread(demoClass));
}
//关闭线程池
pool.shutdown();
九、线程调度(这里摘自百度百科)
计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权。
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解。
java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
一个线程会因为以下原因而放弃CPU。
1 java虚拟机让当前线程暂时放弃CPU,转到就绪状态,使其它线程获得运行机会。
2 当前线程因为某些原因而进入阻塞状态
3 线程结束运行
网友评论