Java>多线程编程

目录

进程与线程

进程与线程的概念

进程与线程的区别

线程状态

创建线程

中断线程

同步

ReentrantLock和Condition

synchronized 

volatile

Java内存模型

原子性、可见性和有序性

原子操作

volatile型变量的特性 可见性,禁止指令重排序,不保证原子性

线程池

ThreadPoolExecutor

线程池的原理

线程池分类

FixedThreadPool

CachedThreadPool

SingleThreadExecutor

ScheduledThreadPool


进程与线程

进程与线程的概念

进程(资源分配的最小单位):是一个动态概念,是竞争计算机系统资源(CPU、内存、IO等)的基本单位,是并发执行的程序在执行过程中分配和管理资源的基本单位。

线程(程序执行的最小单位):是比进程更小的独立运行的基本单位,线程也被称为轻量级进程。是CPU调度和分派的基本单位。

一个程序至少一个进程,一个进程至少一个线程

进程与线程的区别

1、地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间;

2、资源拥有:同一进程内的线程共享本进程的资源,如IO、内存、CPU等,但是进程之间的资源是独立的;

3、所以多进程程序要比多线程更加健壮,多线程程序只要有一个线程死掉,整个进程就死掉;但是一个进程崩溃后,在保护模式下不会对其他进程产生影响,因为进程有自己的独立的地址空间;

4、进程切换时,消耗的资源大,效率高。所以在涉及到频繁切换时,使用线程要比进程好。如果同时进行并且又要共享某些变量的并发操作,只能用线程。

5、执行过程:进程相当于一个应用程序执行的实例,所以每个独立的进程都有一个程序运行的入口、程序顺序执行序列和程序运行出口。每个线程相当于这个应用程序(进程)的一个执行流,所以不能独立执行必须依存在应用程序(进程)之中,由应用程序提供多个线程执行控制;

6、线程是处理器调度的基本单位,但是进程不是;

7、线程和进程都可以并发执行;

线程状态

线程状态
线程状态

Java 线程在运行的声明周期中可能会处于 6 种不同的状态,这 6 种线程状态分别为如下所示。
New :新创建状态。线程被创建,还没有调用 start 方法,在线程运行之前还有一些基础工作要做。
Runnable :可运行状态。一旦调用 start 方法,线程就处于 Runnable 状态。一个可运行的线程可能正在 运行也可能没有运行,这取决于操作系统给线程提供运行的时间。
Blocked :阻塞状态。表示线程被锁阻塞,它暂时不活动。
Waiting :等待状态。线程暂时不活动,并且不运行任何代码,这消耗最少的资源,直到线程调度器 重新激活它。
Timed waiting :超时等待状态。和等待状态不同的是,它是可以在指定的时间自行返回的。
Terminated :终止状态。表示当前线程已经执行完毕。导致线程终止有两种情况:第一种就是 run 方 法执行完毕正常退出;第二种就是因为一个没有捕获的异常而终止了run 方法,导致线程进入终止状态。

创建线程

1.继承Thread类,重写run()方法
2.实现Runnable接口,并实现该接口的run()方法
3.实现Callable接口,重写call()方法

中断线程

void interrupt():中断线程,例如线程A运行时,线程B可以调用线程A的interrupt方法来设置线程A的中断标志位true。注意:这里仅仅是设置了标志,线程A并没有中断,它会继续往下执行。如果线程A调用了wait系列函数,join方法或sleep方法而被阻塞挂起,这时候如果线程B调用了线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回


boolean isInterrupted():检测当前线程是否被中断,如果是返回true,否则返回false.


boolean interrupted():检测当前线程是否被中断,如果是返回true,否则返回false;这个方法是Thread类的静态方法;另外与interrupt()方法的区别在于,如果发现当前线程被中断,则会清除中断 标志;另外需要注意的是:interrupted()方法是获取当前调用线程(正在运行的线程)的中断标志而不是调用interrupted()方法的实例对象的中断标志。(关键字:检测中断标志并清除中断标志)
 

public class Main {
    public static void main(String[] args) {

    }

    public static void test1() {
        Thread t = new Thread(){
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()){
                    System.out.println("thread t running");
                }
                System.out.println("thread t end");
            }
        };
        t.start();
        System.out.println("interrupt thread t end");
        t.interrupt();
    }
    
    public static void test2() {
        Thread t = new Thread(){
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()){
                    System.out.println("thread t running");

                    // 线程在休眠的状态下
                    // 如果catch里不加break,则线程不会中断,只会打印日志
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // 线程在休眠状态下,interrupt会先设置标志位但是又会清除在休眠状态下,
                        // interrupt主要是会触发InterruptedException异常,进入到catch循环里,
                        // 是否终止线程以及如何终止线程的逻辑都可以在catch里实现,这就使得程序对于异常的处理更加灵活
                        e.printStackTrace();
                    }
                }
                System.out.println("thread t end");
            }
        };
        t.start();
        System.out.println("interrupt thread t end");
        t.interrupt();
    }
    
    public static void test3(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()){
                    System.out.println("thread t running");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        //这里直接break,会立即中断线程
                        break;
                    }
                }
                System.out.println("thread t end");
            }
        };
        t.start();
        System.out.println("interrupt thread t end");
        t.interrupt();
    }

}

test2 输出

set thread t end
thread t running
thread t running
java.lang.InterruptedException: sleep interrupted
 at java.lang.Thread.sleep(Native Method)
 at Main$2.run(Main.java:32)
thread t running
thread t running
thread t running
thread t running

同步

在多线程应用中,两个或者两个以上的线程需要共享对同一个数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通常被称为竞争条件。

ReentrantLock和Condition

重入锁ReentrantLock是Java SE 5.0引入的,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。

使用方法

    private final ReentrantLock mLock = new ReentrantLock();
   
    private void doWork()
    {
        final Thread thread = new Thread(() -> {
            // 获得锁
            mLock.lock();
            try {
			//...
            } 
			finally {
                // 解锁
                mLock.unlock();
            }
        });

        thread.start();
    }

发生异常,必须要释放锁,不然后面线程会一直阻塞

条件对象又称作条件变量,当一个线程获得锁后,却发现不满足条件,这时候就需要它。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Main {

    public static void main(String[] args) {
        Main main = new Main();
        main.test();
    }


    private Condition mCondition;

    // 假设的条件
    private boolean mPermission = false;

    public void test()
    {
        // 得到条件对象
        mCondition = mLock.newCondition();

        doWork();
        requestPermission();
    }

    private final ReentrantLock mLock = new ReentrantLock();


    private void doWork()
    {
        final Thread applyThread = new Thread(() -> {
            // 获得锁
            mLock.lock();
            try {
                // 真正工作中,发起授权可能会有些额外代码,这里只做输出。
                System.out.println("正在发起授权");
                while (!mPermission)
                {
                    System.out.println("无授权,进入条件阻塞状态");
                    mCondition.await();
                }

                System.out.println("已获得授权");
            } catch (InterruptedException pException) {
                pException.printStackTrace();
            } finally {
                // 解锁
                mLock.unlock();
            }
        });

        applyThread.start();
    }

    private void requestPermission()
    {
        final Thread permissionThread = new Thread(() -> {
            mLock.lock();
            mPermission = true;
            System.out.println("已允许给予授权");
            // 让所有因此条件进入阻塞的线程进入 可 运行状态。
            mCondition.signalAll();
            mLock.unlock();
        });

        permissionThread.start();
    }


}

代码执行过程:
1. 首先执行doWork(),获得了锁,但条件并不满足
2.  requestPermission()也有同样的锁,同样的锁只能有一个线程,这就说明,doWork()永远无法等待requestPermission()的授权,因为requestPermission()已经被锁阻塞了
3.  这时候,就需要条件阻塞doWork()

synchronized 

一个方法用synchronized 修饰,也就使用了方法所在的对象的锁来保护这个方法

private synchronized void method() {}

 等于

    private final ReentrantLock mLock = new ReentrantLock();
    
    private void method() {
        mLock.lock();
        try {
            // todo
        } finally {
            mLock.unlock();
        }
    }

同步代码快


    public class Demo {

        // 两把不同的锁对象
        private Object object = new Object();
        public  void method(){
            synchronized (object){
                //TODO
            }
        }
    }

volatile

Java内存模型

Java 内存模型定义了线程和主存之间的抽象关系:

线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。

Java 内存模型的抽象示意图

Java内存模型

原子性、可见性和有序性

原子性
对基本数据类型变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行完毕,要么就不执行。
x = 10; 	//语句1
y = x; 		//语句2
x++; 		//语句3

语句1是原子性操作

语句2包含了两个操作,它先读取x的值,再将x的值写入工作内存。读取x的值以及将x的值写入工作内存这两个操作单拿出来都是原子性操作,但是合起来就不是原子性操作了。

语句3包括3个操作:读取x的值、对x的值进行加1、向工作内存写入新值。

一个语句含有多个操作时,就不是原子性操作,只有简单地读取和赋值(将数字赋值给某个变量)才是原子性操作。

ava.util.concurrent.atomic 包中有很多类使用了 很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。例如 AtomicInteger 类提供了方法incrementAndGet和decrementAndGet,它们分别以原子方式将一个整数自增和自减。可以安全地使用AtomicInteger类作为共享计数器而无须同步。
可见性
当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主存,所以对其他线程是可见的。
有序性
Java内存模型中允许编译器和处理器对指令进行重排序,虽然重排序过程不会影响到单线程执行的正确性,但是会影响到多线程并发执行的正确性。这时可以通过volatile来保证有序性,除了volatile,也可以通过synchronized和Lock来保证有序性。

原子操作

read(读取):从主内存读取数据
load(载入):将主内存读取到的数据写入工作内存use(使用):从工作内有读取数据来计算
assign(赋值):将计算好的值重新赋值到工作内存中store(存储):将工作内存数据写入主内存
write(写入):将store过去的变量值赋值给主内存中的变量
lock(锁定):将主内存变量加锁,标识为线程独占状态
unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量


public class Main {

    private static boolean initFlag = false;

    public static void main(String[] args)  throws InterruptedException {

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("initializing data...");
                while (!initFlag) {
                }
                System.out.println("Success...");
            }
        });

        Thread.sleep(2000);

        // 线程2
        new Thread(new Runnable() {
            @Override
            public void run() {
                initFlag = true;
            }
        }).start();
    }

}

主内存中有initFlag变量,初始值是false。

线程1 执行read操作,将false值从主内存中读取到线程1的工作内存,执行load操作将false值放入initFlag变量副本中,执行use 操作从工作内存读取initFlag = false到执行引擎进行计算。

线程2 执行read操作,将false值从主内存中读取到线程2的工作内存,执行load操作将false值放入initFlag变量副本,执行use操作从工作内存读取initFlag = false到执行引擎进行计算,在执行引擎中修改initFlag的值为true,执行assign操作将执行引擎中的true重新赋值给工作内存的initFlag变量副本,使得工作内存中initFlag = true,执行store操作将initFlag = true传递到主内存,执行write操作将true存入到主内存中的initFlag变量。

MESI协议 lock锁的是store到主内存的过程,锁的密度小

线程2执行store操作将initFlag变量副本被修改后的true值传回到主内存并执行write操作将true值写回给主内存的initFlag变量。线程2要将新值写回主内存,会向总线发消息,线程1会通过总线嗅探机制监听到自己工作内存中的initFlag变量被修改了,线程1会立即将自己工作内存的initFlag = false失效,当线程1的执行引擎需要用到initFlag变量时,线程1要重新执行read 和 load操作,那么线程1就能拿到最新值了。
 

volatile型变量的特性 可见性,禁止指令重排序,不保证原子性

可见性

一个线程修改了被volatile修饰的变量,新值对其他线程来说是可以立即得知的。

禁止指令重排序

在多线程并发的场景下,重排序可能会存在问题,最经典的例子就是双重检查锁定单例(Double Check Lock Singleton) 

对共享对象加volatile,可以禁止指令重排序。底层实现是内存屏障

不保证原子性 


public class Main {
    volatile static int x = 1;
    public static void main(String[] args) {
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println(x);
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; ++i) {
                x++;
            }
        }
    }
}

运行结果

13854

count++在执行引擎中被分成了两步操作:

  • count = 0,先将count值初始化为0
  • count=count+1,再执行+1操作

这两步操作在左边的线程执行完第一步,但还没执行第二步时右边的线程抢过CPU控制权开始完成+1的操作后写入到主内存,于是左边的线程工作内存中的count副本失效了,相当于左边这一次+1的操作就被覆盖掉了。

因此,Volatile不能保证原子性。

加锁保证原子性


public class Main {
    volatile static int count = 0;
    static Object object = new Object();

    public static void main(String[] args) {
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println(count);
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; ++i) {

                synchronized (object) {
                    count++;
                }
            }
        }
    }
}

线程池

线程池对线程进行管理。在Java 1.5中提供了Executor框架用于把任务的提交和执行解耦,任务的提交交给Runnable或者Callable,而Executor框架用来处理任务。

ThreadPoolExecutor

通过ThreadPoolExecutor来创建一个线程池,最多参数的构造方法如下所示:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
    }

corePoolSize 核心线程数。默认情况下线程池是空的,只有任务提交时才会创建线程。如果当前运行的线程数少于corePoolSize,则创建新线程来处理任务;如果等于或者多于corePoolSize,则不再创建。


maximumPoolSize 线程池允许创建的最大线程数。如果任务队列满了并且线程数小于
maximumPoolSize时,则线程池仍旧会创建新的线程来处理任务。


keepAliveTime 非核心线程闲置的超时时间。超过这个时间则回收。如果任务很多,并且每个任务的执行事件很短,则可以调大keepAliveTime来提高线程的利用率。另外,如果设置
allowCoreThreadTimeOut属性为true时,keepAliveTime也会应用到核心线程上


TimeUnit keepAliveTime参数的时间单位。可选的单位有天(DAYS)、小时(HOURS)、分钟
(MINUTES)、秒(SECONDS)、毫秒(MILLISECONDS)等。


workQueue 任务队列。如果当前线程数大于corePoolSize,则将任务添加到此任务队列中。该任务队列是BlockingQueue类型的,也就是阻塞队列。这在前面已经介绍过了,这里就不赘述了。


ThreadFactory 线程工厂。可以用线程工厂给每个创建出来的线程设置名字。一般情况下无须设置该参数。


RejectedExecutionHandler 饱和策略。这是当任务队列和线程池都满了时所采取的应对策略,默认是AbordPolicy,表示无法处理新任务,并抛出RejectedExecutionException异常。此外还有3种策略,它们分别如下。
(1)CallerRunsPolicy:用调用者所在的线程来处理任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
(2)DiscardPolicy:不能执行的任务,并将该任务删除。
(3)DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务。 

线程池的原理

(1)提交任务后,线程池先判断线程数是否达到了核心线程数(corePoolSize)。如果未达到核心线程数,则创建核心线程处理任务;否则,就执行下一步操作。
(2)接着线程池判断任务队列是否满了。如果没满,则将任务添加到任务队列中;否则,就执行下一步操作。
(3)接着因为任务队列满了,线程池就判断线程数是否达到了最大线程数。如果未达到,则创建非核心线程处理任务;否则,就执行饱和策略,默认会抛出 RejectedExecutionException异常。 

我们执行ThreadPoolExecutor的execute方法,会遇到各种情况:
(1)如果线程池中的线程数未达到核心线程数,则创建核心线程处理任务。
(2)如果线程数大于或者等于核心线程数,则将任务加入任务队列,线程池中的空闲线程会不断地从任务队列中取出任务进行处理。
(3)如果任务队列满了,并且线程数没有达到最大线程数,则创建非核心线程去处理任务。
(4)如果线程数超过了最大线程数,则执行饱和策略。

线程池分类

FixedThreadPool

FixedThreadPool 是可重用固定线程数的线程池。在 Executors 类中提供了创建 FixedThreadPool 的方法
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
FixedThreadPool corePoolSize maximumPoolSize 都设置为创建 FixedThreadPool 指定的参数 nThreads , 也就意味着FixedThreadPool 只有核心线程,并且数量是固定的,没有非核心线程。 keepAliveTime 设置为 0L 意味着多余的线程会被立即终止。因为不会产生多余的线程,所以keepAliveTime 是无效的参数。
任务队列采用了无界的阻塞队列LinkedBlockingQueue
FixedThreadPool的执行示意图

CachedThreadPool

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
CachedThreadPool corePoolSize 0 maximumPoolSize 设置为 Integer.MAX_VALUE ,这意味着 CachedThreadPool没有核心线程,非核心线程是无界的。 keepAliveTime 设置为 60L ,则空闲线程等待新任务 的最长时间为 60s 。在此用了阻塞队列 SynchronousQueue ,它是一个不存储元素的阻塞队列,每个插入操作 必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。

SingleThreadExecutor

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
corePoolSize和maximumPoolSize都为1,意味着SingleThreadExecutor只有一个核心线程,其他的参数都和FixedThreadPool一样

ScheduledThreadPool

ScheduledThreadPoolExecutor 的构造方法最终调用的是 ThreadPoolExecutor
构造方法。 corePoolSize 是传进来的固定数值, maximumPoolSize 的值是 Integer.MAX_VALUE 。因为采用的 DelayedWorkQueue是无界的,所以 maximumPoolSize 这个参数是无效的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值