多线程的简单理解和使用
多线程的简单理解和使用
1,基础概念
1.1 进程和线程
根本区别:
-
进程:进程是操作系统资源分配的基本单位。
-
线程:线程是任务调度和执行的基本单位。
在开销方面:
-
进程:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销。
-
线程:线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
所处环境:
-
进程:在操作系统中能同时运行多个进程(程序)。
-
线程:在同一个进程(程序)中有多个线程同时执行(通过cpu的调度,在每个时间片,只能有一个线程执行)。
内存分配方面:
-
进程:系统在运行的时候,会为每个进程分配不同的内存空间。
-
线程:除了cpu外,系统不会为线程分配内存,线程组之间只能共享资源。
包含关系:
-
进程:只有一个线程的进程可以看做是单进程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的。
-
线程:线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
1.2 线程的状态
20160531005534_4651.jpg1,新建状态(New):新创建一个线程对象。
2,就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法,该线程就位于可运行线程池中,变得可运行,等待获取cpu的使用权。
3,运行状态(Running):就绪状态的线程获取了cpu的执行权,执行线程的程序代码。
4,阻塞状态(Blocked):线程因为某种原因放弃cpu的使用权,暂时停止线程,直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分为三种:
(1)等待阻塞:运行的线程执行wait()方法,jvm就将线程放入等待池中。
(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程所持有,则jvm会把该线程放入锁池中。
(3)其他阻塞:运行的线程执行sleep()/join()方法,或者发出了I/O请求,jvm会将该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕,线程重新转入就绪状态。
5,死亡状态(Dead):线程执行结束或者因为异常退出了run()方法,该线程结束生命周期。
2,线程的创建方式
2.1 继承Thread类
1,定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
2,创建Thread子类的实例,即创建了线程对象。
3,调用线程对象的start()方法来启动该线程。
//继承Thread类
class MyThread extends Thread{
public void run(){
System.out.println("继承Thread类创建多线程");
}
}
public class Main {
public static void main(String[] args){
//创建并启动线程
new MyThread().start();
}
}
2.2 实现Runnable接口
1,定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
2,创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
3,调用线程对象的start()方法来启动该线程。
//继承Thread类
class MyThread2 implements Runnable{
public void run(){
System.out.println("实现Runnable接口创建多线程");
}
}
public class Main {
public static void main(String[] args){
//创建线程
Runnable myThread2 = new MyThread2();
Thread thread = new Thread(myThread2);
//启动线程
thread.start();
}
}
2.3 实现Callable接口
1,创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
2,创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)
3,使用FutureTask对象作为Thread对象的target创建并启动新线程。
4,调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) {
//执行Callable 方式,需要FutureTask 实现实现,用于接收运算结果
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
new Thread(futureTask).start();
//接收线程运算后的结果
try {
Integer sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.4 使用线程池
线程池提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提交了响应速度。
class ThreadPool implements Runnable {
@Override
public void run() {
for(int i = 0 ;i<10;i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
ThreadPool threadPool = new ThreadPool();
for(int i =0;i<5;i++){
//为线程池分配任务
executorService.submit(threadPool);
}
//关闭线程池
executorService.shutdown();
}
}
3 四种创建方式的区别
- 使用继承Thread类创建多线程:
优点:编写简单,如果需要访问当前线程,直接使用this即可获得当前线程。
缺点:线程类已经继承了Thread类,无法再继承其他的类。
- 采用实现Runnable、Callable接口的方式创建多线程。
优点:线程类只实现了Runnable/Callable接口,还可以继承其他类,多个线程可以共享同一个target对象,所以非常适合多个相同线程处理同一份资源的情况。
缺点:编程稍微复杂,如果要访问当前线程,需要调用Thread.currentThread()方法。
- Runnable和Callable的区别:
1,Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
2, Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
3,call方法可以抛出异常,run方法不可以。
4,运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
- 使用线程池创建线程:
优点:
1,实现线程池的重用
2,可以控制线程池的并发数
3,线程池可以对线程进行管理
缺点:
1,有引发死锁的风险
2,资源不足
3,并发错误
4,线程泄露
5,请求过载
4,线程同步 ---- 'synchronized'
synchronized:可以修饰方法,也可以修饰代码块。
实现原理:
本质上是一个对象的监视器的获取。任意一个对象都拥有自己的监视器,当同步方法或者同步代码块时,执行方法的线程必须先获得该对象的监视器才能进入同步方法或者同步代码块,没有获得监视器的线程将会被阻塞,并进入同步队列,状态变为BLOCKED。当成功获取监视器的线程释放了锁后,会唤醒阻塞在同步队列的线程,使其重新获取监视器。
synchronized关键字也可以修饰静态方法
,此时如果调用该静态方法
,将会锁住整个类
。
下面补充一点:可重入锁
和ReentrantLock
可重入锁:
当一个线程得到一个对象后,再次请求该对象锁可以再次得到该对象的锁的,具体概念是:自己可以再次获取自己的内部锁。
Java里面内置锁(synchronized)和Lock(ReentrantLock)都是可重入的。
ReentrantLock:
需要显示加锁或者释放锁。
常用方法:
void lock(): 执行该方法时,如果锁处于空闲状态,当前线程将获取到锁,相反,如果锁已经被其他线程持有,则禁用当前线程,直到当前线程获得锁。
boolean tryLock():如果锁可用,则获取锁,返回true,否则返回false,该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用,当前线程仍然继续往下执行代码. 而lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行。
void unlock():执行此方法时,当前线程将释放持有的锁,锁只能由持有者释放,如果当前线程并不持有锁,却执行该方法,可能导致异常的发生。
Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁。
synchronized
和ReentrantLock
的区别:
1,可中断锁:顾名思义,就时可以相应中断的锁。在Java中,synchronized就是不可中断锁,而Lock时可中断锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。lockInterruptibly()的用法体现了Lock的可中断性。
2,公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请的时间顺序来依次获得锁;而非公平锁则不能保证这一点。非公平锁在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过带布尔值的构造函数要求使用公平锁。
3,死锁:synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
4,读写锁:读写锁将对一个资源的访问分为两个锁,一个读锁,一个写锁。正因为有了读写锁,才使得多个线程之间的读操作可以并发进行,不需要同步,而写操作需要同步进行,提高了效率。ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。
5,绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多余一个条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这么做,只需要多次调用new Condition()方法即可。
5,补充
5.1 volatile
java中的实例对象、数组元素都放在java堆中,Java堆时线程共享的,我们把共享的内存称为主内存,每个线程线程私有的内存称为工作内存,当线程1和线程2需要通信时,需要经过以下几个步骤:
1,线程一工作内存将x=1,执行store操作,对主内存实行write操作将x=1写入主内存。
2,线程2实行read操作读取主内存的X=1,工作内存实行load操作,将X=1加载入自己的工作内存。
存在的问题:由于工作内存这个中间层的出现,线程1和线程2必然存在延迟的问题,例如线程1在工作内存中更新了变量,但还没刷新到主内存,而此时线程2获取到的变量值就是未更新的变量值,又或者线程1成功将变量更新到主内存,但线程2依然使用自己工作内存中的变量值,同样会出问题。
volatile
的作用:
多处理器开发中保证了共享变量的"可见性",可见性是当一个线程修改一个共享变量时,另外一个线程能读取到这个修改的值。
1,一个变量被volatile修饰后,则表示本地内存无效,每次读取需要从主内存读取。
2,当一个volatile变量被修改后,则立即写入主内存中。
实现:
加入volatile关键字后,会多出一个lock前缀指令,lock前缀指令其实就相当于一个内存屏障。内存屏障时一组处理指令,用来实现对内存操作的顺序限制,避免JMM重排序,xolatile的底层就是通过内存屏障来实现的。使用场景:状态修改;单例模式双检锁。
5.2 ThreadLocal
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
实现源码解读:
每个线程有个threadLocals的属性,这个属性是ThreadLocalMap类型,这个map的key:threadLocal对象,value:储存的值的对象。
public class ThreadLocalTest {
// 定义一个全局变量 来存放线程需要的变量
public static ThreadLocal<Integer> ti = new ThreadLocal<Integer>();
public static void main(String[] args) {
// 创建两个线程
for(int i=0; i<2;i++){
new Thread(new Runnable() {
@Override
public void run() {
Double d = Math.random()*10;
// 存入当前线程独有的值
ti.set(d.intValue());
new A().get();
new B().get();
}
}).start();
}
}
static class A{
public void get(){
// 取得当前线程所需要的值
System.out.println(ti.get());
}
}
static class B{
public void get(){
// 取得当前线程所需要的值
System.out.println(ti.get());
}
}
5.3 concurrent
atomic [java.util.concurrent.atomic]
1,原子类其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法(同步的工作更多的交给了硬件),从而避免了synchronized的高开销,执行效率大为提升
2,虽然基于CAS的线程安全机制很好很高效,但要说的是,并非所有线程安全都可以用这样的方法来实现,这只适合一些粒度比较小,型如计数器这样的需求用起来才有效,否则也不会有锁的存在了。
3,在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装类。
6,小结
本篇文章简单介绍了java基础中的多线程,由于纯手打,难免会有纰漏,如果发现错误的地方,请第一时间告诉我,这将是我进步的一个很重要的环节。