JAVA高并发程序设计--读中总结与思考(day1)
闲来蛋疼,刷书 总结,本书是读 “JAVA高并发程序设计”-读后的一些总结与思考
一、走入并行世界
1.1、摩尔定律:每隔18-24个月,计算机性能翻一倍 -- 但是由于科技等客观原因,并没有完全实现
从硬件--到软件,从单核到多核。
1.2、概念
1.2.1、同步与异步 :同步和异步通常用来形容一次方法调用。同步等待返回后继续;异步调用直接返回,更像消息传递者,异步通常会在另一个线程中继续执行。同步类似现场买商品和异步类似网购商品。
1.2.2、并发和并行:并发:交替执行。并行:同时执行。单核CPU使用多线程不可能真正并行。真实并行只能出现在多个CPU的系统中,也就是多核CPU
1.2.3、临界区:表示公共资源或共享数据,可以被多个线程使用。但每一次只能有一个线程使用它,一旦被占用其他线程只能等待。如办公司一台打印机只能在同一时间只能完成一个任务。
1.2.4、阻塞和非阻塞: 形容多线程间的相互影响
临界资源被某个线程占用,其他线程要使用该临界资源,就会导致线程挂起,这就是阻塞;非阻塞意思是多个线程之间互相不影响。
1.2.5、死锁、饥饿、活锁
死锁:互相占有对方的资源而不愿让出自己当前资源的情况。
饥饿:线程因为各种原因无法获得所需要的资源的情况。如由于优先级较低或被其他线程长时间占用导致无法获取资源。
活锁:线程之间互相避让,都在等对方先执行,但都无法同时拿到所有资源的情况。
1.3、并发级别:阻塞、无饥饿、无障碍、无锁、无等待
1.3.1、阻塞:线程阻塞是由于线程所需的资源被其他资源占用,synchronized关键字或者重入锁等到的就是阻塞线程
1.3.2、无饥饿:非公平锁-由于线程优先级问题,可能导致饥饿;公平锁--按照先进先出原则,按顺序运行,无饥饿。
1.3.3、无障碍:多个线程可以无障碍执行,不会因为临界区的问题导致一方被挂起。共享数据若被同时修改,则修改数据的线程都将进行回滚。可行的无障碍实现可以依赖一个“一致性标记”来实现,若改标记在线程操作完成前没有被修改,则表明资源没有冲突。否则表示数据不再安全,需要进行回滚。
1.3.4、无锁:无锁的并行都是无障碍的。所有线程都能尝试对临界进行访问,无锁的并发必然有一个线程能在有限步内完成操作离开临界区。
1.3.5、无等待:无等待要求所有的线程必须在有限步内完成。典型无等待结构:RCU(read Copy Update) 。对数据的读不加控制,写数据的时候先取得原始副本,接着只修改副本数据,修改完成后在合适的时机写数据。
1.4、定律:原文有两个定律,一个是指优化并行的效率是有限的;另一个是要想达到某一个目标,只需要后面执行一些条件就可以达到;解释:如汽车在不同路上行驶速度不同,哪怕再好的路段依旧是有限的速度。后者解释:初始速度较低,但是只要时间以及后面的速度超过我们所期望的平均速度,那么只要给定足够的时间也能达到平均速度。
1.5、JAVA内存模型 JMM
1.5.1、原子性:原子性是指一个操作是不可中断的,及时在多个线程一起执行的时候,一个操作一旦开始就不会被其他线程影响;如静态的全局变量 int i; 两个线程同时对它进行赋值,A线程赋值1,B线程赋值-1,线程A和线程B之间不管怎么调用,互相是没有干扰的。但是如果改为long类型数据,对于32位系统来读写来说,就是非原子性的(因为long是64位的)
写到这里,贴一下基本类型信息
包装类型 | 基本类型 | 数据类型 | 字节 | 位数 | 默认值 | 取值范围 |
---|---|---|---|---|---|---|
Byte | byte | 字节型 | 1 | 8 | 0 | -2^7 ~ 2^7-1 |
Short | short | 短整型 | 2 | 16 | 0 | -2^15 ~ 2^15-1 |
Integer | int | 整型 | 4 | 32 | 0 | -2^31 ~ 2^31-1 |
Long | long | 长整型 | 8 | 64 | 0 | -2^63 ~ 2^63-1 |
Float | float | 单精度浮点型 | 4 | 16 | 0.0F | 3.4E-38 ~ 3.4E+38 |
Double | double | 双精度浮点型 | 8 | 64 | 0.0D | 1.7E-38 ~ 1.7E+38 |
Character | char | 字符型 | 2 | 16 | 0 | 0~65535 |
Boolean | boolean | 布尔型 | 1 | 8 | FALSE | true,false |
1.5.2、可见性:可见性是指当某一个线程修改了一个共享变量时,其他线程是否能够立即知道这个修改。
可见性经常由于缓存优化、硬件优化、指令重排以及编译器优化都有可能导致一个线程的修改不会立即被其他线程察觉。
指令重排的原因:性能原因,尽量减少中断流水线,尽量消除停顿(中断和停顿是由于相关条件还未准备好)。一条指令的执行分为很多步.
- 取指IF
- 译码和取寄存器操作数ID
- 执行或有效地址计算EX
- 存储器访问MEM
- 写回WB
指令重排原则 Happen-Before规则
- 程序顺序原则:一个线程语义的串行性
- volatile 规则: volatile 变量的写先于读发生,保证volatile 变量的可见性;数据被单个线程修改后,其他线程能看到改动
- 锁规则: 解锁(unlock)必然发生在随后的加锁(lock)前
- 传递性 : A先于B,B先于C,那么A一定先于C。
- 线程的start方法先于他的每一个动作
- 线程的中断(interrupt() ) 先于被中断的代码。
- 对象的构造函数的执行、结束先于 finalize() 方法。
二、并行程序基础
2.1、线程的状态 NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED
new 线程刚刚创建,未运行,等待start()方法调用才执行
runnable 执行状态,线程执行时
blocked :线程执行中遇到synchronized 同步块,线程就进入阻塞状态
waiting : 等待状态--无时间限制等待,除非等到notify (随机唤醒一个线程,可能导致线程永远不执行) 或者notifyAll 唤醒,
timed_waiting:有时间限制等待。等待都是等待一些特殊事件,如通过join唤醒。
terminated : 线程执行完后,进入结束状态
2.2、线程的基本操作
- 新建线程:
通过继承Thread 类 或者实现runnable 接口,Thread 类有一个非常重要构造方法: public Thread(Runnable target) - 终止线程:
一般线程执行完毕后就会结束。如果强行调用线程的stop()方法 -- 已被废弃。可能会导致不一致问题,因为在并行程序中,Thread.stop() 方法结束线程会直接终止线程,并立即释放这个线程所持有的锁,而锁恰巧是用来维持对象一致性的。写数据一半被终止那么数据将会被写坏。
友好终止线程: 自定义一个stopMe() 方法,设置一个全局Boolean变量参数,需要是调用即可,如下
package com.example.demo.thread;
/**
* @Author: xx
* @Date: 2019/12/16 10:55
* @Desc: 友好的停止线程
*/
public class StopThreadFriendlyTest extends Thread {
// 使用volatile 修饰,保证一个线程修改数据后其他线程能够看到这个改动
volatile boolean stopme = false;
public static DemoUser user = new DemoUser();
public void stopMe() {
stopme = true;
}
@Override
public void run() {
while (true) {
if (stopme) {
System.out.println("exit me by stop me");
break;
}
// 加锁保证一致性
synchronized (user) {
int id = (int) (System.currentTimeMillis() / 1000);
user.setId(id);
try {
Thread.sleep(100);
} catch (InterruptedExceptione) { // 如果线程休眠时中断线程,会抛出当前异常
System.out.println("Interrupted when sleep ");
// 需要后续处理来保证数据的一致性和完整性
Thread.currentThread().isInterrupted();// 设置中断状态 不处理下次循环无法捕捉该状态
}
user.setName(String.valueOf(id));
}
// 线程让步 执行后会吧自己CPU时间让掉,让自己或者其他线程运行,并不是单纯让给其他线程。
// 线程从运行状态修改为就绪状态。
// 线程优先级越高,最先执行的几率越高,但并不一定是先执行
Thread.yield();
}
}
}
- 线程休眠:
Thread.sleep() , 线程休眠时中断会抛出 InterruptedException 异常,非运行时异常。所以必须捕获并处理。样例代码如上 - 线程中断:
线程并不一定立即退出,是发送通知到线程,告知目标线程,希望你退出了。目标线程接到通知的测处理方式由目标线程决定。
线程中断易混淆方法:
// 中断线程
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
// 判断是否中断,并清除当前中断状态
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
// 判断是否中断
public boolean isInterrupted() {
return isInterrupted(false);
}
线程的中断需要添加相关的中断处理逻辑,否则中断不起作用
代码示例如下
package com.example.demo.thread;
/**
* @Author: xiaoxiao
* @Date: 2019/12/16 11:19
* @Desc: 线程重点解释
*/
public class ThreadInterruptedTest {
// 线程t1没有中断处理逻辑,所以调用线程中断对当前线程无效
Thread t1 = new Thread(){
@Override
public void run() {
while (true){
System.out.println("thread yield begin");
Thread.yield();
}
}
};
/**
* 线程t2有相关的判断中断逻辑,当调用中断时会退出循环体结束线程
*/
Thread t2 = new Thread(){
@Override
public void run() {
while (true){
// 下面代码判断线程是否中断
if (Thread.currentThread().isInterrupted()){
System.out.println("thread is interrupted");
break;
}
System.out.println("thread yield begin");
Thread.yield();
}
}
};
}
wait() 和 notify() 等待和通知
wait()和notify()方法都是object类的方法。
当对象实例上调用wait() 方法时,当前线程就会在这个对象上等待,并且释放object对象的监视器,需要注意的是wait()方法不能随便调用,必须包含在对应的synchronized语句中。
调用wait()方法的,线程进入等待队列,后续再调用notify()或者notifyAll方法,如果线程线程被唤醒后,先尝试获得object对象的监视器,如果暂时无法获得则继续等待,若获得该object类的监视器,则该线程才能执行。
wait和sleep的区别
object的wait方法和thread的sleep方法都可以让线程等待若干时间,wait方法可以被唤醒,同时会释放目标对象锁(资源),sleep方法不会释放任何资源。
挂起(suspend)和继续执行(resume)方法差别
不推荐使用suspend()方法是因为:挂起导致线程停止并不会释放任何资源,只能等待resume()方法才能继续执行,容易导致系统工作不正常。线程被挂起状态任然是runnable状态,会导致状态判断失败。
等待线程结束join和谦让yeild
join可以指定等待时间或者无限等待
// 不限时间的无限等待下去,必须要等待目标线程执行完毕
public final void join() throws InterruptedException
// 指定等待具体时间多久
public final synchronized void join(long millis)
yield()方法:使当前线程让出CPU。但是让出CPU并不代表不继续执行,让出后还是回进行CPU资源争夺;使用场景:对优先级较低的线程使用。
join使用样例:如下,有一个主线程和线程t1 jion使用保证输出的结果是0.
如果t1不调用join(),由于指令重排等因素输出可能是0
package com.example.demo.thread;
/**
* @Author: xiaoxiao
* @Date: 2019/12/16 15:46
* @Desc: 线程join测试
*/
public class ThreadJoinTest {
public volatile static int i = 0;
public static class AddThread extends Thread {
@Override
public void run() {
for (i = 0; i < 10000; i++){}
}
}
public static void main(String[] args) throws InterruptedException {
AddThread t1 = new AddThread();
t1.start();
// 此处调用join方法 保证线程t1执行完之后再继续执行主线程
t1.join();
System.out.println(i);
}
}
线程组
相同功能的线程放在同一个线程组中。
package com.example.demo.thread;
/**
* @Author: xiaoxiao
* @Date: 2019/12/16 16:20
* @Desc: 线程组测试
*/
public class ThreadGroupTest implements Runnable {
public static void main(String[] args) throws InterruptedException {
// 创建线程组并命名
ThreadGroup threadGroup = new ThreadGroup("pring group test ");
Thread t1 = new Thread(threadGroup, new ThreadGroupTest(), "T1");
Thread t2 = new Thread(threadGroup, new ThreadGroupTest(), "T2");
t1.start();
t2.start();
System.out.println(" 活动线程总数 : " + threadGroup.activeCount());
// 输出线程组所有信息
threadGroup.list();
}
@Override
public void run() {
String groupAndName = Thread.currentThread().getThreadGroup().getName() + "- " + Thread.currentThread().getName();
System.out.println("I am " + groupAndName);
try {
System.out.println("这个是线程:" + Thread.currentThread().getName());
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出的结果为:
C:\Program Files\Java\jdk1.8.0_201\bin\java.exe" ...
活动线程总数 : 2
java.lang.ThreadGroup[name=pring group test ,maxpri=10]
Thread[T1,5,pring group test ]
Thread[T2,5,pring group test ]
I am pring group test - T2
这个是线程:T2
I am pring group test - T1
这个是线程:T1
Process finished with exit code 0
守护线程 (daemon)
比如:垃圾回收和JIT线程可以理解为守护线程。与之相对应的是用户线程,用户线程可以意味作系统的工作线程。若用户线程全部结束,则意味着程序无事可做,守护线程守护对象不存在,整个应用程序结束。因此当一个应用内只有守护线程时,java虚拟机会自然退出
守护线程示例:下述示例大概会打印1-3次 "I am alive "
package com.example.demo.thread;
/**
* @Author: xiaoxiao
* @Date: 2019/12/16 17:09
* @Desc: 守护线程测试
*/
public class ThreadDaemonTest {
public static class DaemonT extends Thread {
@Override
public void run() {
while (true) {
System.out.println("I am alive ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
DaemonT daemonT = new DaemonT();
// 必须在线程启动前设置线程为守护线程 否则可能会导致线程daemonT 为非守护线程导致主线程休眠2秒后还在一直一直打印
daemonT.setDaemon(true);
daemonT.start();
// 设置主线程休眠两秒
Thread.sleep(2000);
}
}
线程优先级
用1-10表示,数字越大优先级越高,数字越小优先级越低。
优先级越高,竞争资源时更有优势,但是并不代表一定能抢夺到资源
设置优先级: Thread.setPriority();括号内填1-10整数,如果不是1-10整数,会抛出IllegalArgumentException异常
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
synchronized 关键字
实现线程间同步,对同步代码加锁,使得每一次只有一个线程进入同步块,从而保证线程间的安全性。
用法:
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
- 直接用户实例方法: 相当于对当前实例加锁,进入实例前要获得当前实例的锁。
- 直接用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁
以下描述代码是非线程安全的,运行的结果大部分情况是小于10000000 * 2 的,若在代码中加锁 synchronized (test) 或synchronized (ThreadSynchronizedTest.class)均可以保证线程安全。同理也可以采用前面描述的使用join来保证
package com.example.demo.thread;
/**
package com.example.demo.thread;
/**
* @Author: xiaoxiao
* @Date: 2019/12/17 10:33
* @Desc: synchronized测试
*/
public class ThreadSynchronizedTest implements Runnable {
static ThreadSynchronizedTest test = new ThreadSynchronizedTest();
// 使用volatile修饰表示该字段修改后对其他线程是可见的。
static volatile int i = 0;
// public static synchronized void increase() {
// 若将下面一行代码替换为上面代码,也能保证线程安全
public static void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
// 使用以下任一一种方式可以保证线程安全
// synchronized (test){
// increase();
// }
// synchronized (ThreadSynchronizedTest.class){
// increase();
// }
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
// 若将t1.join() 提取到当前位置,即时run代码没有加锁也能保证线程安全
// t1.join();
t2.start();
// 这个地方使用join是为了保证线程执行完之后再输出 i
t1.join();
t2.join();
System.out.println(i);
}
}
不同线程之间同步可以使用 静态方法 + 锁的方式实现,如下代码
package com.example.demo.thread;
/**
* @Author: xiaoxiao
* @Date: 2019-12-17 11:06:18
* @Desc: synchronized测试
*/
public class ThreadSynchronizedTest implements Runnable {
static volatile int i = 0;
// 静态加锁方式 实现线程间同步
public static synchronized void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new ThreadSynchronizedTest());
Thread t2 = new Thread(new ThreadSynchronizedTest());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
2.3 程序中隐蔽的错误
- 无提示错误:两个整数求均值 数据越界问题
- 并发下ArrayList 越界异常和值最终不准确问题
- 并发下HashMap : 程序正常结束,结果不符合预期;程序永远无法结束--链表被破坏导致结构成环,程序永远无法停止。
示例代码由于时间原因后续更新