多线程与并发下.md

9、生产者与消费者应用案例

sleep() : 让线程进入休眠状态,让出CPU的时间片,不释放监视器的所有权(对象锁)

wait() : 让线程进入等待状态,让出CPU的时间片,释放监视器的所有权,等待其他线程通过notify方法来唤醒

10、线程生命周期

调用start() 方法进入就绪状态,获取到CPU的时间片,调用run()方法会进入运行状态,

如果运行状态调用yield() 方法,会进入就绪,让出一片时间片,

调用join() 和sleep() 方法会进入blocked 状态,join()或sleep() 结束会重新进入runnable

runnable方法调用wait 也会进入blocked 状态, 阻塞状态会进入锁定状态,

wait() 的blocked 状态的通过调用notify() 进入锁定

11、线程池

JDK1.5 后,提供了线程的线程池,Java 里面线程池的顶级接口使Executor,是一个执行线程的工具

线程池接口是ExecutorService。

java.util.concurrent 包,并发编程中很常用的使用工具

Executor 接口:

执行已提交的Runnable 任务的对象

ExecutorService 接口

提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状态而生成Future的方法

Executors类:

此包中所定义的Executor、 ExecutoService 等工厂和使用方法

newSingleThreadExecutor

创建一个单线程的线程池,这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行

newFixedThreadPool

创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。

线程池的大小一旦达到最大值就会保持不变

如果某个线程因为执行异常而结束,那么线程池会补充一个新的线程

newCachedThreadPool

创建一个可缓存的线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行)的线程,当任务数增加时,此线程池又可能智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(JVM)能够创建的最大线程大小。(比较少用,)

newScheduledThreadPool

创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求

	public static void main(String[] args){
//        ExecutorService es = Executors.newSingleThreadExecutor();
        ExecutorService es = Executors.newFixedThreadPool(3);
        es.execute(new MyRunnable6());
        es.execute(new MyRunnable6());


        es.shutdown();
    }

附加

public class Test1 {
    private int a=1, b=2;
    public void foo(){  // 线程1 
        a=3;
        b=4;
    }
    public int getA(){ // 线程2
        return a;
    }    
    public int getB(){ // 线程2
        return b;
    }

b=4语句可能比a=3语句先执行,

  1. java 编译器的重排序(Reording)操作有可能导致执行顺序和代码顺序不一致

假设有两条语句,代码执行顺序是语句1先于语句2,那么只要语句2不依赖于语句1,打乱它们的顺序对最终结果没有影响的话,那么它们的顺序没有限制。

  1. 从线程工作内存写回主存时顺序无法保证,

JVM中主存和线程工作内存之间的交互,线程再修改一个变量时,先拷贝入线程工作内存,在线程工作内存修改后写回主存。线程1 把变量写回Main Memery 的过程对线程2的可见性顺序无法保证。Jvm 中一个重要的问题就是如何让多个线程之间,对象的状态对于各线程的”可视性“顺序一致,它的解决方法就是Happens-before 规则:要想保证执行动作B的线程看到动作A的结果,A和B之间就必须满足happens-before

java1.5之前Java 中的锁只有基本的synchronized, java 5之后,增加了一些其他锁,比如ReentranLock,它基本作用和synchronized 相似,但提供了更多的操作方式,比如在获取锁时不必向sybchronized 那样傻等,可以设置定时,轮询,或者中断,使得他在获取多个锁的情况可以避免死锁的操作。

java 1.5 ReentranLock 的性能比sybchronized 来说有很大提高,java1.6对sybchronized 进行了优化,现在两者差不多,ConcurrentHashMap 中,每个哈市区间使用的锁正是ReentranLock,1.8后ConcurrentHashMap 使用的又是sybchronized 了。

不变模式(immutable) 是多线程安全里最简单的一种保障方式,主要通过final关键字来限定。

HashTable 容器在竞争激烈的并发环境表现出效率低下的原因,是所有访问HashTable 的线程必须竞争同一把锁,假如每一把锁用于锁住容器中的一部分,那么当多线程访问容器中不同部分的时候,就不会存在锁的竞争。

ConcurrentHashMap 由Segment 数组结构和HashEntry 数组结构组成,Segment 是一种锁,HashEntry 用于存储键值对。每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

java 内存模型与volatile关键字

A,B两个线程如果要实现通信的话,必须实现两个步骤:

  1. 线程A把本地内存A中更新过的共享变量刷新到主内存中
  2. 线程B读取主内存中共享变量,拷贝一份副本到本地内存B

volatile 关键字语义:

  1. 任何一个线程对该变量进行修改后,其他线程都立即可见,保证了可见性
  2. 禁止指令重新排序,保证了有序性

保证可见性的过程如下:

  1. 使用volatile关键字会强制更新后的共享数据立即刷新到主内存
  2. 当其他线程修改了被volatile修饰的共享数据,此线程上的工作内存上该数据的副本会失效,强制此线程去主内存读取更新后的数据

保证有序性:

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

语句1和语句2一定在语句3前面,语句4和语句5一定在语句3后面。

synchronized 和volatile 的比较

如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

volatile 的内存语义:如果一个变量被volatile 关键字修饰时,那么对这个变量写的就是本地内存中拷贝刷新到共享内存中,对这个变量的读会有一些不同,读的时候是无视他的本地内存的拷贝,只是从共享变量中读取数据

synchronized: 读和写是加锁的,别的线程无法对这个变量进行读写操作

public class RunThread extends Thread {
    private boolean isRunning = true;
 
    public boolean isRunning() {
        return isRunning;
    }
 
    public void setRunFlag(boolean flag) {
        isRunning = flag;
    }
 
    @Override
    public void run() {
        System.out.println("I'm come in...");
        boolean first = true;
        while(isRunning) {
            if (first) {
                System.out.println("I'm in while...");
                first = false;
            }
        }
        System.out.println("I'll go out.");
    }
    // 

public class MyRun {
    public static void main(String[] args) throws InterruptedException {
        RunThread thread = new RunThread();
        thread.start();
        Thread.sleep(100);
        thread.setRunFlag(false);
        System.out.println("flag is reseted: " + thread.isRunning());
    }
    
    // 虽然有thread.setRunFlag(false);, 但是并不会happens-before,虽然主线程中对isRunning 进行了修改,然而对子线程中的while 来说,并没有改变,所以这就引发在while 中的死循环
    //虽然对象以及成员变量分配的内存是在共享内存中的,不过对于每个线程而言,还是可以拥有这个对象的拷贝,这样做的目的是为了加快程序的执行,这也是现代多核处理器的一个显著特征。从上面的内存模型可以看出,Java的线程是直接与它自身的工作内存(本地内存)交互,工作内存再与共享内存交互。这样就形成了一个非原子的操作
    //这里工作内存被 while 占用,无法去更新主线程对共享内存 isRunning 变量的修改。所以,如果我们想要打破这种限制,可以通过 volatile 关键字来处理。通过 volatile 关键字修饰 while 的条件变量,即 isRunning。

// 修改成这样
private volatile boolean isRunning = true;

这样子线程每次读取isRunning, 就去主内存中读取, 不再工作内存中读

volatile 原子性测试

public class DemoNoProtected {
 
    static class MyThread extends Thread {
        static int count = 0;
 
        private static void addCount() {
            for (int i = 0; i < 100; i++) {
                count++;
            }
            System.out.println("count = " + count);
        }
 
        @Override
        public void run() {
            addCount();
        }
    }
 
    public static void main(String[] args) {
        MyThread[] threads = new MyThread[100];
        for (int i = 0; i < 100; i++) {
            threads[i] = new MyThread();
        }
 
        for (int i = 0; i < 100; i++) {
            threads[i].start();
        }
    }
}
/*output

count = 300
count = 300
count = 300
count = 400
... ...
count = 7618
count = 7518
count = 9918
*/

即使我们添加了volatile 关键字, 也不行,甚至最后输出的不是10000

因为我们都知道,count++ 是这样的

int tmp = count;
tmp = tmp + 1;
count = tmp;

可见,count++ 不是原子性操作

ThreadLocal

ThreadLocal 修饰的变量一般称为线程本地变量,特殊的线程绑定机制,将对象的可见范围限制在同一线程内,

public class Solution {
	public static void main(String[] args) {
		TestDemo d1 = new TestDemo();
		Thread t1 = new Thread(d1);
		Thread t2 = new Thread(d1);
		Thread t3 = new Thread(d1);
		Thread t4 = new Thread(d1);
		Thread t5 = new Thread(d1);
		Thread t6 = new Thread(d1);
		t3.start();
		t1.start();
		t2.start();
		t4.start();
		t6.start();
		t5.start();
	}
}
 
class TestDemo implements Runnable {
	ThreadLocal<Integer> a = new  ThreadLocal<Integer>() {
		protected Integer initialValue() {
			return 0;
		}
	};
	@Override
	public void run() {
		a.set(a.get()+1);
		System.out.println(Thread.currentThread().getName()+","+a.get());
	}
	

可以看到ThreadLocal中有一个静态内部类ThreadLocalMap;
其实从get()方法中你就可以看出,该map是根据每个线程存一个值来保存变量副本的。

锁池: 假设线程A已经拥有了某个对象的锁,而其他线程想要调用这个对象的某个sybchronized方法,由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就会进入了该对象的锁池中。(记录着被阻塞的线程,也就是想用加锁资源的线程)

等待池: 假设一个线程A调用了某个对象的wait() 方法,线程A就会释放该对象的锁,进入到该对象的等待池中

notify 和notifyAll 有什么区别:

  • 如果线程调用了对象的wait() 方法,那么线程便会处于对象的等待池中,等待池中的线程不会去竞争该对象的锁
  • 当有线程调用了对象的notifyAll() 方法(唤醒所有的wait线程)或notify() 方法(只随机唤醒一个wait线程),被唤醒的线程便会进入该对象的锁池中,锁池中的线程会去经侦该对象锁,也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll 会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait() 方法,它才会重新回到等待池中,而竞争到对象锁的线程则继续往下执行,直到执行完了sybchronized代码块,它会释放该对象锁,这时锁池中的线程会继续竞争该对象锁。

综上,所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。

有了这些理论基础,后面的notify可能会导致死锁,而notifyAll则不会的例子也就好解释了

信号量 semaphore

(1)如果一个线程要访问一个共享资源,他必须先获得信号量。如果信号量的内部计数器大于0,信号量将减1,然后允许访问这个共享资源。计数器大于0意味着又可以使用的资源,因此线程讲被允许使用其中一个资源。

(2)如果信号量等于0,信号将将会把线程植入休眠直到计数器大于0.计数器等于0的时候意味着所有的共享资源已经被其他线程使用了,所以需要访问这个共享资源的线程必须等待。

(3)当线程使用完这个共享资源时,信号量必须被释放,以便其他线程能够访问共享资源,释放操作将使用信号量的内部计数器增加1。

并发集合

java提供了两类适用于并发应用的集合

阻塞式集合: 当集合已满或为空,添加或移除方法不能立即执行,那么调用这个方法的线程会被阻塞,一直到方法可以被成功执行

非阻塞式集合: 当集合已满或为空,添加或移除方法不能立即执行,会返回null 或抛出异常,不会被阻塞


非阻塞式列表对应的实现类:ConcurrentLinkedDeque

阻塞式列表对应的实现类:LinkedBlockingDeque

用于数据生成或者消费的阻塞式列表对应的实现类:LinkedTransferQueue

按优先级排序列表元素的阻塞式列表对应的实现类:PriorityBlockingQueue

带有延迟列表元素的阻塞式列表对应的实现类:DelayQueue

非阻塞式列表可遍历映射对应的饿实现类:ConcurrentSkipListMap

随机数字对应的实现类:ThreadLockRandom

原子变量对应的实现类:AtomicLong和AtomicIntegerArray

JDK常用的包

java.lang(唯一一个不需要导入的包) java.io java.net java.util java.sql

String s = “a”+“b”+“c”+“d”;共创建了多少个对象?

1个对象。由于编译器对字符串常量直接相加的表达式进行了优化。编译时即可去掉+号,直接将其编译成一个常量相连的结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值