Java并发编程的重要以及难点(个人总结一)

在学习Java并发编程的过程是要注意以下几点:
1.java并发的三个特性
2.volatile、synchronized、lock,重入锁、读写锁的区别。
3.线程间的通信机制
4.线程池
5.阻塞队列
6.ConcurrentHashMap原理以及几个方法运用
7.sleep、wait、Thread.join的区别
这篇博文主要就是围绕这七点进行总结

1.java并发的三个特性:

1. 原子性

即一个或者多个操作作为一个整体,要么全部执行,要么都不执行,并且操作在执行过程中不会被线程调度机制打 断;而且这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换(context switch)。

int i = 0;//语句1
i++;//语句2

语句1是一个原子性操作。
语句2的分解步骤是:
1)获取 i 的值;
2)计算 i + 1 的值;
3)将 i + 1 的值赋给 i;
执行以上3个步骤的时候是可以进行线程切换的,因此语句2不是一个原子性操作

2. 可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。

private int i = 0;
private int j = 0;
//线程一
i = 10;
//线程二
j = i;

线程1修改i的值为10时的执行步骤:
1)将10赋给线程1工作内存中的 i 变量;
2)将线程1工作内存中的 i 变量的值赋给主内存中的 i 变量;
当线程2执行j = i时,线程2的执行步骤:
1)将主内存中的 i 变量的值读取到线程2的工作内存中;
2)将主内存中的 j 变量的值读取到线程2的工作内存中;
3)将线程2工作内存中的 i 变量的值赋给线程2工作内存中的 j 变量;
4)将线程2工作内存中的 j 变量的值赋给主内存中的 j 变量;
如果线程1执行完步骤1,线程2开始执行,此时主内存中 i 变量的值仍然为 0,那么线程2获取到的 i 变量的值为 0,而不是 10。
这就是可见性问题,线程1对 i 变量做了修改之后,线程2没有立即看到线程1修改的值。
(哈哈 有点绕口啊)
3. 有序性

int i = 0;
int j = 0;
i = 11;
j = 3;

语句可能的执行顺序如下:
1)语句1 语句2
2)语句2 语句1
语句1一定在语句2前面执行吗?答案是否定的,这里可能会发生执行重排(Instruction Reorder)。一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序在单线程环境下最终执行结果和代码顺序执行的结果是一致的。
比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

总结:一个正确执行的并发程序,必须具备原子性、可见性、有序性。否则就有可能导致程序运行结果不正确,甚至引起死循环。

2.volatile、synchronized、lock,重入锁、读写锁的区别。

volatile:
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  2)禁止进行指令重排序。
使用volatile的场景:
1.对变量的写操作不依赖于当前值
2.该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
synchronized
synchronized是java典型的线程同步控制方法了。
synchronized两种使用方法:
1.synchronized修饰方法
2.synchronized修饰代码块
具体表现为:
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象。
实现方法:
java对象头和monitor是实现synchronized的基础
Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)
Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。
关于锁的比较,从书上有这么一个表格。
Java并发编程的重要以及难点(个人总结一)_第1张图片
lock锁
lock锁拥有了锁获取与释放的可操作性,可中断的获取锁,超时获取锁。
lock锁与synchronized的区别
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到finally{} 中;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
重入锁
重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。
读写锁
之前提到锁基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被堵塞。

3.线程间的通信机制

1.同步
同步就是上面所说的通过各种锁实现线程的并发。
2.while轮询的方式
while轮询方式就是B线程中有个while死循环一直等待着A线程的消息。
3.wait/notify机制
wait/notif机制就是等待/通知机制
一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。
Java并发编程的重要以及难点(个人总结一)_第2张图片
ps(使用wiat()、notify()、notifyAll()时需要先调用对象加锁)
4.管道通信
使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信

4.线程池

使用线程池的三个好处:
1.降低资源消耗
2.提高响应速度
3.提高线程的可管理性
Java并发编程的重要以及难点(个人总结一)_第3张图片
这个图可以反映线程池实现原理。
线程池创建有六个参数
new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,milliseconds,runnableTaskQueue,handler)
1>corePoolSize:线程池的基本大小
2>maximumPoolSize:线程池最大数量
3>keepAliveTime:线程活动保持时间
4>milliseconds:允许核心线程超时
5>runnableTaskQueue:任务队列
6>handler:任务拒绝处理器
向线程池提交任务:
1.execute(无返回值)
2.submit(有返回值)
简单的demo:


public class MyTask implements Runnable {
    private int taskNum;
    public MyTask(int num){
        this.taskNum = num;
    }
    @Override
    public void run() {
        System.out.println("正在执行task "+taskNum);
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task "+taskNum+"执行完毕");
    }

}

主类:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPool {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,200,TimeUnit.MILLISECONDS,new ArrayBlockingQueue(5));
        for(int i = 0; i < 15; i++) {
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);
            System.out.println("线程中线程数目"+executor.getPoolSize()+",队列中等待执行的任务数目:"+executor.getQueue().size()+",已执行完别的任务数目:"+executor.getCompletedTaskCount());
        }
        executor.shutdown();
    }
}

5.阻塞队列

几种主要的阻塞队列:
rrayBlockingQueue:基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。

LinkedBlockingQueue:基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。

PriorityBlockingQueue:以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。

DelayQueue:基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
阻塞队列解释:
阻塞队列是一个队列,当您尝试从队列中出队并且队列为空时,或者尝试将项目排入队列并且队列已满时,该队列会被阻止。试图从空队列中出队的线程被阻塞,直到其他线程将一个项插入到队列中。尝试将队列排入队列中的线程会被阻塞,直到某个其他线程在队列中产生空间为止,或者通过将一个或多个项目出队或完全清除队列。

6.ConcurrentHashMap原理以及几个方法运用

由于本人实力有限,就不在解释ConcurrentHashMap的源代码了。
ConcurrentHashMap有hashmap的效率又有hashtable的线程安全,所以ConcurrentHashMap很优秀。ConcurrentHashMap是由Segment数组结构和HashEntry数据结构组成
Segment是一种可重入锁,在ConcurrentHashMap中扮演锁的角色,HashEntry则用于存储键值对数据
ConcurrentHashMap的三个操作。
1.get操作
get操作的高效之处在于整个get过程中不用加锁,除非读到的值是空才会加锁重读。为什么呢?因为get方法里将要用的共享变量都定义为volatile类型,定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值)、
2.put操作
由于put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁,put方法首先定位到Segment,然后在Segment进行插入操作。
(1)是否需要扩容
在插入元素前会先判断Segment里的HashEntry数组是否超过容量,如果超过,就扩容。
(2)如何扩容
在扩容的时候首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入新的数组里。
3.size操作
先尝试两次不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。

7.sleep、wait、Thread.join的区别

1.sleep来自Thread类,wait来自Object类
2.sleep方法没有释放锁,wait释放锁,使得其他进程可以使用同步控制块或者方法。
3.wait、notify、notifyAll只能在同步控制方法或者同步控制块里使用,sleep可以在任何地方使用。
4.sleep必须捕获异常,wait、notify、notifyAll不需要。

Thread.join的定义:
如果一个线程A执行了thread.join语句,其含义:当前线程A等待Thread线程终止后才从thread.join返回。
Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行

以上就是本人对Java并发编程难点的一点点小总结,以后还会继续更新,如果不足和错误还请多多指出,谢谢!

你可能感兴趣的:(Java,并发编)