多线程、并发(很乱)

先复习下

 

我们先了解一下Thread的几个重要方法。

a、start()方法,调用该方法开始执行该线程;

b、stop()方法,调用该方法强制结束该线程执行;

c、join方法,调用该方法等待该线程结束,用于主线程等待子线程执行结束。

d、sleep()方法,调用该方法该线程进入等待。

e、run()方法,调用该方法直接执行线程的run()方法,但是线程调用start()方法时也会运行run()方法,区别就是一个是由线程调度运行run()方法,一个是直接调用了线程中的run()方法!!

 

自己的理解:

    首先object提供了wait和notify,网上很多都讲得非常奇怪

    这两个方法是必须要跟synchronize一起用的,否则会异常。

 

  • wait方法:被锁的对象.wait()表示,正在使用该锁的线程,不再拥有该锁,所以线程也就停止,进入了等待notify唤醒的block区。
  • notify:被锁的对象.notify()表示,在block区中找一个优先级高的线程唤醒,进入runnable,等待获得锁

所以,它和sleep是没有可比性的,sleep是操控线程,而它是操控对象

并发编程 锁

 Java中的锁

  •  synchronized
  •  Lock

Lock接口最常用的: 

1. ReentrantLock


/**
 * 执行结果:
 *  线程B得到了锁...
 *  线程B释放了锁...
 *  线程A得到了锁...
 *  线程A释放了锁...
 */
public class LockTest {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        final LockTest test = new LockTest();

        new Thread("A") {
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();

        new Thread("B") {
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }

    public void insert(Thread thread) {
          // 注意这个地方:lock被声明为局部变量
        lock.lock();
        try {
            System.out.println("线程" + thread.getName() + "得到了锁...");
            for (int i = 0; i < 5; i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {

        } finally {
            System.out.println("线程" + thread.getName() + "释放了锁...");
            lock.unlock();
        }
    }
}

2. ReentrantReadWriteLock  读写锁

/**
 * Thread-0正在进行读操作
 * Thread-0正在进行读操作
 * Thread-1正在进行读操作
 * Thread-1正在进行读操作
 * Thread-1正在进行读操作
 * Thread-1正在进行读操作
 * Thread-1正在进行读操作
 * Thread-1正在进行读操作
 * Thread-1正在进行读操作
 * Thread-0正在进行读操作
 * Thread-1读操作完毕
 * Thread-0读操作完毕
 * 读写锁并不会真正去检查你是否在读 是否在写
 * 关键在于你怎么用它
 */
public class ReadLockTest {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    private List<String> ls = new ArrayList<>();

    public static void main(String[] args) {
        final ReadLockTest test = new ReadLockTest();

        final Thread thread = new Thread() {
            public void run() {
                test.get(Thread.currentThread());
            }
        };


        final Thread thread1 = new Thread() {
            public void run() {
                test.get(Thread.currentThread());
            }
        };
        thread.start();
        thread1.start();

    }

    public void get(Thread thread) {
        System.out.println(thread.getName());
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();

            while (System.currentTimeMillis() - start <= 30) {
                System.out.println(thread.getName() + "正在进行读操作");
            }
            System.out.println(thread.getName() + "读操作完毕");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

 

 锁类型

  • 可重入锁:在执行对象中所有同步方法不用再次获得锁

  • 可中断锁:在等待获取锁过程中可中断

  • 公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利

  • 读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写


 

synchronized与Lock的区别

1、我把两者的区别分类到了一个表中,方便大家对比:

类别

synchronized

Lock

存在层次

Java的关键字,在jvm层面上

是一个类

锁的释放

1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁

在finally中必须释放锁,不然容易造成线程死锁

锁的获取

假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待

可以尝试获得锁,线程可以不用一直等待

锁状态

无法判断

可以判断

锁类型

可重入 不可中断 非公平

可重入 、可判断 、可公平(两者皆可)、 可中断

性能

差不多

差不多

***** synchronized不可中断,所以性能有时会比较差,此时可以用Lock接口的ReentrantLock  用interrupt中断

 

 

 

多线程中断机制

在 java中启动线程非常容易,大多数情况下是让一个线程执行完自己的任务然后自己停掉。

一个线程在未正常结束之前, 被强制终止是很危险的事情. 因为它可能带来完全预料不到的严重后果,比如会带着自己所持有的锁而永远的休眠,迟迟不归还锁等

但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程

 

如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作(阻塞、限期等待或者无限期等待状态),那么调用线程的 interrupt() 方法就无法使线程提前结束(如IO阻塞、sync操作)

 

线程中断是一种协作机制,调用线程对象的interrupt方法并不一定就中断了正在运行的线程,它只是要求线程自己在合适的时间中断自己。

 

 

await() signal() signalAll()

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

使用 Lock 来获取一个 Condition 对象。

public class AwaitSignalExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    AwaitSignalExample example = new AwaitSignalExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}

before
after

很明显,相对于synchronized,Lock这种可以控制多个condition的方式,更加方便。

Java内存模型

 

Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。

主内存与工作内存

处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。

加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。

 

所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。

 

线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。

 

内存间交互操作

 

Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。

 

  • read:把一个变量的值从主内存传输到工作内存中

  • load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中

  • use:把工作内存中一个变量的值传递给执行引擎

  • assign:把一个从执行引擎接收到的值赋给工作内存的变量

  • store:把工作内存的一个变量的值传送到主内存中

  • write:在 store 之后执行,把 store 得到的值放入主内存的变量中

  • lock:作用于主内存的变量

  • unlock

 

 

内存模型三大特性

1.原子性            

Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性

有一个错误认识就是,int 等原子性的变量在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 变量属于 int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。

 

2. 可见性

可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。

volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。

对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。

3. 有序性

有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。

在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。

也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。

 

J.U.C - AQS

 

java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。

CountdownLatch

用来控制一个线程等待多个线程。

维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。

 

public class CountdownLatchExample {

    public static void main(String[] args) throws InterruptedException {
        final int totalThread = 10;
        CountDownLatch countDownLatch = new CountDownLatch(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("run..");
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        System.out.println("end");
        executorService.shutdown();
    }
}

run..run..run..run..run..run..run..run..run..run..end

 

 

volatile关键字

1.volatile保证可见性

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

 

 

  • 第一:使用volatile关键字会强制将修改的值立即写入主存;

  • 第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  • 第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

2.volatile不能确保原子性

3.volatile保证有序性 

volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

 

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  • 在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

 

1

2

3

4

5

6

7

8

9

//线程1:

context = loadContext();   //语句1

inited = true;             //语句2

 

//线程2:

while(!inited ){

  sleep()

}

doSomethingwithconfig(context);

有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

volatile的应用场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

下面列举几个Java中使用volatile的几个场景。

①.状态标记量

1

2

3

4

5

6

7

8

9

volatile boolean flag = false;

 //线程1

while(!flag){

    doSomething();

}

  //线程2

public void setFlag() {

    flag = true;

}

根据状态标记,终止线程。

②.单例模式中的double check

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

class Singleton{

    private volatile static Singleton instance = null;

 

    private Singleton() {

 

    }

 

    public static Singleton getInstance() {

        if(instance==null) {

            synchronized (Singleton.class) {

                if(instance==null)

                    instance = new Singleton();

            }

        }

        return instance;

    }

}

为什么要使用volatile 修饰instance?

主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

1.给 instance 分配内存

2.调用 Singleton 的构造函数来初始化成员变量

3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值