Java多线程并发探索

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yus201120/article/details/81875911

1、什么是线程?
在解答什么是线程之前,我们要清楚什么是进程。进程是程序代码的一次动态执行,是系统进行资源分配和调度的一个独立单位。线程是进程中的一个执行单元,是CUP调度和执行的基本单位。

  • 一个进程可以有多个线程,但至少拥有一个线程;一个线程必定需要有一个父进程。
  • 线程之间可以共享父进程的共享资源,协同完成任务。
  • 一个线程可以创建和撤销另一个线程,进程中的线程可以同时并发执行。

2、线程相关的一些方法

  • sleep()方法让当前正在执行的线程在指定时间内暂停执行,正在执行的线程可以通过Thread.currentThread()方法获取。
  • yield()方法放弃线程持有的CPU资源,将其让给其他任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。
  • wait()方法是当前执行代码的线程进行等待,将当前线程放入预执行队列,并在wait()所在的代码处停止执行,知道接到通知或者被中断为止。该方法可以使得调用该方法的线程释放共享资源的锁, 然后从运行状态退出,进入等待队列,直到再次被唤醒。该方法只能在同步代码块里调用,否则会抛出IllegalMonitorStateException异常。
  • wait(long millis)方法等待某一段时间内是否有线程对锁进行唤醒,如果超过了这个时间则自动唤醒。
  • notify()方法用来通知那些可能等待该对象的对象锁的其他线程,该方法可以随机唤醒等待队列中等同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态。
  • notifyAll()方法可以是所有正在等待队列中等待同一共享资源的全部线程从等待状态退出,进入可运行状态,一般会是优先级高的线程先执行,但是根据虚拟机的实现不同,也有可能是随机执行。

3、Java的线程内存模型
JVM中存在一个主内存区,对于所有线程共享,而每个线程又有自己的工作内存区,工作内存中保存的某些变量是主存区中对应变量的拷贝副本,线程对所有变量的操作仅发生在工作内存中,而线程之间的内存是不能相互访问的,变量在程序中的传递,是依赖主存来完成的。

  • 原子性 :在 Java 中就是指一些不可分割的操作。只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
x = 1;
y = x;

上面的代码,第一行是原子操作,直接把数字赋值给了x;第二行包含两步操作,首先读取x的值(因为 x 是变量),然后再把值赋给y,所以不是原子操作。

  • 可见性 :指一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
    Java内存模型默认是不可见的,也就是说线程A修改了共享变量的值之后,线程B并不能马上知道,线程B感知这个共享变量的值的时机是不确定的。
  • 有序性 :即程序执行的顺序按照代码的先后顺序执行。
    为什么强调有序性,因为有时候编译器和处理器为了优化程序性能而会对指令进行重新排序,这就是重排序。重排序会使得我们的多线程操作属于不可控的状态。

4、线程安全
线程安全就是指某个方法在多线程环境被调用的时候,能够正确处理多个线程之间的共享变量,使程序功能能够正确执行。
然而在实际的程序开发中,我们如果不处理多线程并发问题的话,那么由于线程间的不可见性、对变量的非原子性操作、指令重排序等问题,经常会导致共享变量最后的结果不是我们想要的值。

5、volatile关键字
volatile也是互斥同步的一种实现,不过它非常的轻量级。
volatile有两条关键的语义:

  • 保证被volatile修饰的变量对所有线程都是可见的
  • 禁止进行指令重排序

被volatile修饰的变量在工作内存中改变后会被强制刷写到主内存中,其他线程需要用到这个变量的时候也必须重新从主内存中获取并刷新。同时也禁止了指令重排序,使执行按照我们编写的程序顺序执行。
但是被volatile关键字修饰变量后,却并不能完全保证线程安全,因为它并不能保证操作的原子性。

6、synchronized
当某个线程访问被synchronized标记的方法或代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个 线程才会释放该对象的锁,其他线程才能执行这个方法或代码块。
JMM关于synchronized的两条规定:

  • 线程解锁前,必须把工作内存中变量的值刷新到主内存中;
  • 线程加锁时,须清空工作内存中共享变量值,从而使用共享变量的时候需要从主存中读取变量最新的值。

    每个Java对象都可以用作实现同步的互斥锁,这些锁被称之为内置锁。线程进入同步代码块或方法时自动获得内置锁,退出同步代码块或方法时自动释放该内置锁。进入同步代码块或者同步方法是获得内置锁的唯一途径。

    synchronized的几种用法:

  • 实例同步方法 :synchronized修饰实例方法(非静态方法)的时候,执行该方法需要获取该对象的内置锁(同一个类的不同实例拥有不同的内置锁)。如果该实例的多个方法被synchronized修饰,同时有多个线程调用不同的方法的时候,需要竞争锁。调用不同实例的不同方法的时候,不需要竞争锁。

  • 静态同步方法:synchronized修饰类的静态方法的时候,执行该方法需要获取的是这个类的class对象的锁(一个类只有一个class对象)。调用同一个类中不同静态同步方法的时候需要竞争锁。
  • 同步代码块:synchronized修饰的代码块执行的时候,需要获得synchronized括号后面对象(可以是实例对象也可以是类的class对象)的内置锁。

    锁的使用是为了操作临界资源的正确性,而往往一个方法中并非所有的代码都操作临界资源。换句话说,方法中的代码往往并不都需要同步。此时建议不使用同步方法,而使用同步代码块,只对操作临界资源的代码,也即需要同步的代码加锁。这样做的好处是,当一个线程在执行同步代码块时,其它线程仍然可以执行该方法内同步代码块以外的部分,充分发挥多线程并发的优势,从而相较于同步整个方法而言提升性能。

7、Java中的锁

  • 重入锁 :Java的重如锁(ReenTrantLock)和Java内置锁一样,都是排它锁。在使用Synchronized的地方一定可以用ReenTrantLock代替。
try{
  renentrantLock.lock();
  // 用户操作
} finally {
  renentrantLock.unlock();  //防止死锁,通常在finally里面释放锁
}
  • 读写锁 :锁可以保证原子性和可见性,而原子性更多是针对写操作而言。在读多写少的场景中,我们没有必要让一个读操作去阻塞另一个读操作,只需要读和写不同时发生或者写和写不同时发生即可。
    一个ReentrantReadWriteLock实例包含一个ReentrantReadWriteLock.ReadLock实例和一个ReentrantReadWriteLock.WriteLock实例。通过readLock()和writeLock()方法可分别获得读锁实例和写锁实例,并通过Lock接口提供的获取锁方法获得对应的锁。
    读写锁的规则如下:

    • 获得读锁后,其它线程可获得读锁而不能获取写锁
    • 获得写锁后,其它线程既不能获得读锁也不能获得写锁

8、信号量
信号量维护一个许可集,可通过acquire()获取许可(若无可用许可则阻塞),通过release()释放许可,从而可能唤醒一个阻塞等待许可的线程。信号量可以允许同一时间多个线程访问特定资源,所以信号量并不能保证原子性。

参考:http://www.jasongj.com/java/multi_thread/
https://github.com/guoxiaoxing/android-open-source-project-analysis/blob/master/doc/

猜你喜欢

转载自blog.csdn.net/yus201120/article/details/81875911
今日推荐