多线程


  • 多线程
  • What is 多线程
  • Why is 多线程
  • 线程安全
  • 对象的发布和逸出
  • 安全发布对象
  • How to 多线程
  • 创建多线程
  • Thread的API
  • 解决线程安全性
  • 大致解决线程安全的方法
  • 三大特性
  • 原子性
  • 可见性
  • 有序性
  • 线程封闭

What is 多线程

介绍线程就得说一下线程

进程

进程是程序的⼀次执⾏,进程是⼀个程序及其数据在处理机上顺序执⾏时所发⽣的活动,进程是具有独⽴功能的程序在⼀个数据集合上运⾏的过程,它是系统进⾏资源分配和调度的⼀个独⽴单位

  • 进程是系统进行资源分配和调度的独立单位。每⼀个进程都有它⾃⼰的内存空间和系统资源

线程

进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较⼤的时间和空间开销,线程就是为了提高系统的执⾏效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。

  • 进程实现多处理⾮常耗费CPU的资源,而我们引入线程是作为调度和分派的基本单位

进程和线程

可以简单的说:

  • 进程作为资源分配的基本单位
  • 线程作为资源调度的基本单位

并行:

  • 并⾏性是指同⼀时刻内发⽣两个或多个事件。
  • 并⾏是在不同实体上的多个事件

并发:

  • 并发性是指同⼀时间间隔内发⽣两个或多个事件。
  • 并发是在同⼀实体上的多个事件

并行是针对进程的,并发是针对线程的。


线程有3个基本状态

  • 执⾏、就绪、阻塞

线程有5种基本操作

  • 派⽣、阻塞、激活、 调度、 结束

线程有两个基本类型

  1. ⽤户级线程:管理过程全部由用户程序完成,操作系统内核心只对进程进⾏管理。
  2. 系统级线程(核心级线程):由操作系统内核进行管理。

Why is 多线程

使用多线程主要是为了提高系统的资源利用率。现在CPU基本都是多核的,如果你只用单线程,那就是只用到了一个核心,其他的核心就相当于空闲在那里了。

在操作系统系统中,我们可以知道IO操作对于CPU是非常慢的。所以如果我们需要做类似IO这种慢的操作,可以开多个线程出来,尽量不要让CPU空闲下来,提高系统的资源利用率。

当然并不是线程开的越多越好,执行IO操作我们线程可以适当多一点,因为很多时候CPU是相对空闲的。如果是计算型的操作,本来CPU就不空闲了,还开很多的线程就不对了(有多线程就会有线程切换的问题,线程切换都是需要耗费资源的)

线程安全

线程安全不是指线程的安全,而是指内存的安全。在一个进程中,所有的线程都可以访问该进程的内存。所以线程安全是指在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险。

对象的发布和逸出

发布和逸出:

发布(publish) 使对象能够在当前作⽤域之外的代码中使用

逸出(escape) 当某个不应该发布的对象被发布了

逸出的下面几种方式:

  • 静态域逸出
  • public修饰的get方式
  • 方法参数传递
  • 隐式的this

静态域逸出:

java 线程数字 java 线程数量_多线程

public修饰get⽅法:

java 线程数字 java 线程数量_#多线程_02

安全发布对象

  • 静态域中直接初始化
  • 静态初始化由JVM在类的初始化阶段就执行了,JVM内部存在着同步机制,致使这种⽅式我们可以安全发布对象
  • 对应的引用保存到volatile或者AtomicReferance引⽤中
  • 保证了该对象的引⽤的可⻅性和原⼦性
  • final修饰
  • 该对象是不可变的,那么线程就⼀定是安全的,所以是安全发布
  • 由锁来保护
  • 发布和使⽤的时候都需要加锁,这样才保证能够该对象不会逸出

How to 多线程

创建多线程

  • 继承 Thread类,重写run函数。
public class ThreadDemo01 extends Thread{
    public ThreadDemo01(){
        //编写子类的构造方法,可缺省
    }
    public void run(){
        //编写自己的线程代码
        System.out.println(Thread.currentThread().getName());
    }
    public static void main(String[] args){ 
    	ThreadDemo01 threadDemo01 = new ThreadDemo01(); 
        threadDemo01.setName("我是自定义的线程1");
        threadDemo01.start();
        System.out.println(Thread.currentThread().toString());  
    }
}
  • 实现 Runnable接口
public class ThreadDemo02 {
 
    public static void main(String[] args){ 
        Thread t1 = new Thread(new Runnable() {
            
            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println(Thread.currentThread().getName() + " : 这是接口的线程");
            }
        });
        t1.start(); 
        System.out.println(Thread.currentThread().getName());
    }
}
  • 实现 Callable接口
public class ThreadDemo03 {
 
    public static void main(String[] args) {
        // TODO Auto-generated method stub
 
        FutureTask<Object> oneTask = new FutureTask<Object>(new Callable<Object>() {

            @Override
            public java.lang.Object call() throws Exception {
                // TODO Auto-generated method stub
                System.out.println(Thread.currentThread().getName()+"-->我是通过实现Callable接口通过FutureTask包装器来实现的线程");
                return null;
            }
        });

        Thread t = new Thread(oneTask);
        t.start();
        System.out.println(Thread.currentThread().getName());
 
    }
 
}
  • 通过线程池创建线程
public class ThreadDemo05{
 
    private static int POOL_NUM = 10;     //线程池数量
 
    /**
     * @param args
     * @throws InterruptedException 
     */
    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        ExecutorService executorService = Executors.newFixedThreadPool(5);  
        for(int i = 0; i<POOL_NUM; i++)  
        {  
            Thread.sleep(1000);
            executorService.execute(new Runnable() {
                @Override
                public void run()  
                {  
                    System.out.println("通过线程池方式创建的线程:" + Thread.currentThread().getName() + " ");  
                }
            });
        }
        //关闭线程池
        executorService.shutdown(); 
    }
}

run()和start()方法的区别

  • run() :仅仅是封装被线程执行的代码,直接调⽤是普通⽅法
  • start() :首先启动了线程,然后再由jvm去调⽤该线程的run()⽅法。

Thread的API

线程名

  • 查看线程名

Thread.currentThread().getName()

  • 修改线程名

Thread.currentThread().setName()

默认线程名字:主线程叫做main,其他线程是Thread-x

守护线程

守护线程是为其他线程服务的

守护线程的特点

  • 当别的⽤户线程执⾏完了,虚拟机就会退出,守护线程也就会被停⽌掉了。
  • 守护线程作为⼀个服务线程,没有服务对象就没有必要继续运⾏了

守护线程的注意事项:

  • 在线程启动前设置为守护线程,⽅法是 setDaemon(boolean on)
  • 使⽤守护线程不要访问共享资源(数据库、⽂件等),因为它可能会在任何时候就挂掉了。
  • 守护线程中产⽣的新线程也是守护线程

优先级线程

线程优先级⾼仅仅表示线程获取的CPU时间⽚的⼏率⾼,Java提供的优先级默认是5,最低是1,最⾼是10:

线程生命周期

线程基础状态有三种,在Java中分为6种,NEW(初始)、RUNNABLE(运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(定时等待)、TERMINATED(终止)

  1. NEW(初始):线程被创建后尚未启动。
  2. RUNNABLE(运行):包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在运行,也可能正在等待系统资源,如等待CPU为它分配时间片。
  3. BLOCKED(阻塞):线程阻塞于锁。
  4. WAITING(等待):线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. TIME_WAITING(超时等待):该状态不同于WAITING,它可以在指定的时间内自行返回。
  6. TERMINATED(终止):该线程已经执行完毕。

Thread上很多的方法都是⽤来切换线程的状态。

  • sleep方法

调⽤sleep⽅法会进⼊计时等待状态

  • yield⽅法

调⽤yield⽅法会先让别的线程执⾏,但是不确保真正让出

  • join⽅法

调⽤join⽅法,会等待该线程执行完毕后才执行别的线程

  • interrupt⽅法

没有强制线程终⽌的⽅法,之前可以使用stop()强行中断,目前该方法已经过时了。

  • ⼀般使⽤的是interrupt来请求终⽌线程,注意:
  • interrupt不会真正停⽌⼀个线程,它仅仅是给这个线程发了一个信号告诉它,它应该要结束了

java 线程数字 java 线程数量_#Java_03


java 线程数字 java 线程数量_#多线程_04

解决线程安全性

大致解决线程安全的方法

  • ⽆状态(没有共享变量)
  • 使⽤final使该引⽤变量不可变(如果该对象引⽤也引⽤了其他的对象,那么⽆论是发布或者使⽤时
    都需要加锁)
  • 加锁(内置锁,显示Lock锁)
    使⽤JDK为我们提供的类来实现线程安全(此部分的类就很多了)
  • 原子性(就count++ 操作,可以使用AtomicLong来实现原子性,那么在增加的时候就不会出差错了!)
  • 容器(ConcurrentHashMap等等...)
  • 使用synchronized、volatile关键字

三大特性

原子性

原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。

在多线程中,因为很多时候都是因为某个操作不是原子性的,因此导致数据混乱出错。

所以,在多线程中为了保证线程安全,我们需要使一个非原子的操作变为原子的操作。Java的concurrent包下提供了一些原子类。如下图:

java 线程数字 java 线程数量_java 线程数字_05

网上也有表格:

类型

Integer

Long

基本类型

AtomicInteger

AtomicLong

AtomicBoolean

数组类型

AtomicIntegerArray

AtomicLongArray

AtomicReferenceArray

属性原子修改器

AtomicIntegerFieldUpdater

AtomicLongFieldUpdater

AtomicReferenceFieldUpdater

可见性

可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。

对于可见性,Java提供了一个关键字:volatile

volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性。普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。

使用volatile修饰的变量保证了以下三点

  • ⼀旦你完成写入,任何访问这个字段的线程将会得到最新的值
  • 在你写入前,会保证所有之前发⽣的事已经发⽣,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写⼊值都刷新到缓存。
  • volatile可以防止重排序(重排序指的就是:程序执行的时候,CPU、编译器可能会对执⾏顺序做⼀些调整,导致执行的顺序并不是从上往下的。从而出现了⼀些意想不到的效果)

因此,一般在下列情况才使用volatile修饰变量:

  • 修改变量时不依赖变量的当前值(因为volatile是不保证原⼦性的)
  • 该变量不会纳入到不变性条件中(该变量是可变的)
  • 在访问变量的时候不需要加锁(加锁就没必要使⽤volatile这种轻量级同步机制了)
有序性

Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的如果在一个线程中观察另一个线程,所有操作都是无序的

前半句是指“线程内表现为串行语义”。

后半句是指“指令重排序”现象和“工作内存中主内存同步延迟”现象。

Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。

  • volatile关键字本身就包含了禁止指令重排序的语义。
  • 而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。

先行发生原则

先行发生原则是指Java内存模型中定义的两项操作之间的依序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包含了修改了内存中共享变量的值、发送了消息、调用了方法等。

下面是Java内存模型下一些规定好的先行发生关系:

  • 程序次序规则(Pragram Order Rule)

在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。

  • 管程锁定规则(Monitor Lock Rule)

一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面“是指时间上的先后顺序。

  • volatile变量规则(Volatile Variable Rule)

对一个volatile变量的写操作先行发生于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。

  • 线程启动规则(Thread Start Rule)

Thread对象的start()方法先行发生于此线程的每一个动作。

  • 线程终于规则(Thread Termination Rule)

线程中的所有操作都先行发生于对此线程的终止检测

  • 线程中断规则(Thread Interruption Rule)

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。

  • 对象终结规则(Finalizer Rule)

一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。

  • 传递性(Transitivity)

如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

常言:三思而後行。一思善惡是非,二思公私他己,三思盈虧損余。