多线程
- 多线程
- What is 多线程
- Why is 多线程
- 线程安全
- 对象的发布和逸出
- 安全发布对象
- How to 多线程
- 创建多线程
- Thread的API
- 解决线程安全性
- 大致解决线程安全的方法
- 三大特性
- 原子性
- 可见性
- 有序性
- 线程封闭
What is 多线程
介绍线程就得说一下线程
进程
进程是程序的⼀次执⾏,进程是⼀个程序及其数据在处理机上顺序执⾏时所发⽣的活动,进程是具有独⽴功能的程序在⼀个数据集合上运⾏的过程,它是系统进⾏资源分配和调度的⼀个独⽴单位
- 进程是系统进行资源分配和调度的独立单位。每⼀个进程都有它⾃⼰的内存空间和系统资源
线程
进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较⼤的时间和空间开销,线程就是为了提高系统的执⾏效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。
- 进程实现多处理⾮常耗费CPU的资源,而我们引入线程是作为调度和分派的基本单位
进程和线程
可以简单的说:
- 进程作为资源分配的基本单位
- 线程作为资源调度的基本单位
并行:
- 并⾏性是指同⼀时刻内发⽣两个或多个事件。
- 并⾏是在不同实体上的多个事件
并发:
- 并发性是指同⼀时间间隔内发⽣两个或多个事件。
- 并发是在同⼀实体上的多个事件
并行是针对进程的,并发是针对线程的。
线程有3个基本状态:
- 执⾏、就绪、阻塞
线程有5种基本操作:
- 派⽣、阻塞、激活、 调度、 结束
线程有两个基本类型:
- ⽤户级线程:管理过程全部由用户程序完成,操作系统内核心只对进程进⾏管理。
- 系统级线程(核心级线程):由操作系统内核进行管理。
Why is 多线程
使用多线程主要是为了提高系统的资源利用率。现在CPU基本都是多核的,如果你只用单线程,那就是只用到了一个核心,其他的核心就相当于空闲在那里了。
在操作系统系统中,我们可以知道IO操作对于CPU是非常慢的。所以如果我们需要做类似IO这种慢的操作,可以开多个线程出来,尽量不要让CPU空闲下来,提高系统的资源利用率。
当然并不是线程开的越多越好,执行IO操作我们线程可以适当多一点,因为很多时候CPU是相对空闲的。如果是计算型的操作,本来CPU就不空闲了,还开很多的线程就不对了(有多线程就会有线程切换的问题,线程切换都是需要耗费资源的)
线程安全
线程安全不是指线程的安全,而是指内存的安全。在一个进程中,所有的线程都可以访问该进程的内存。所以线程安全是指在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险。
对象的发布和逸出
发布和逸出:
发布(publish) 使对象能够在当前作⽤域之外的代码中使用
逸出(escape) 当某个不应该发布的对象被发布了
逸出的下面几种方式:
- 静态域逸出
- public修饰的get方式
- 方法参数传递
- 隐式的this
静态域逸出:
public修饰get⽅法:
安全发布对象
- 在静态域中直接初始化
- 静态初始化由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(终止)
- NEW(初始):线程被创建后尚未启动。
- RUNNABLE(运行):包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在运行,也可能正在等待系统资源,如等待CPU为它分配时间片。
- BLOCKED(阻塞):线程阻塞于锁。
- WAITING(等待):线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING(超时等待):该状态不同于WAITING,它可以在指定的时间内自行返回。
- TERMINATED(终止):该线程已经执行完毕。
Thread上很多的方法都是⽤来切换线程的状态。
- sleep方法
调⽤sleep⽅法会进⼊计时等待状态
- yield⽅法
调⽤yield⽅法会先让别的线程执⾏,但是不确保真正让出
- join⽅法
调⽤join⽅法,会等待该线程执行完毕后才执行别的线程
- interrupt⽅法
没有强制线程终⽌的⽅法,之前可以使用stop()强行中断,目前该方法已经过时了。
- ⼀般使⽤的是interrupt来请求终⽌线程,注意:
- interrupt不会真正停⽌⼀个线程,它仅仅是给这个线程发了一个信号告诉它,它应该要结束了
解决线程安全性
大致解决线程安全的方法
- ⽆状态(没有共享变量)
- 使⽤final使该引⽤变量不可变(如果该对象引⽤也引⽤了其他的对象,那么⽆论是发布或者使⽤时
都需要加锁) - 加锁(内置锁,显示Lock锁)
使⽤JDK为我们提供的类来实现线程安全(此部分的类就很多了)
- 原子性(就count++ 操作,可以使用AtomicLong来实现原子性,那么在增加的时候就不会出差错了!)
- 容器(ConcurrentHashMap等等...)
- 使用synchronized、volatile关键字
三大特性
原子性
原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
在多线程中,因为很多时候都是因为某个操作不是原子性的,因此导致数据混乱出错。
所以,在多线程中为了保证线程安全,我们需要使一个非原子的操作变为原子的操作。Java的concurrent包下提供了一些原子类。如下图:
网上也有表格:
类型 | 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的结论。
常言:三思而後行。一思善惡是非,二思公私他己,三思盈虧損余。