Java高级:多线程编程
多线程编程
- 多进程编程
进程(process):是操作系统调用和运行的基本单位!!
在一定程度上,我们可以认为一个进程就是一个软件(这句话是有问题!!)
一个进程中,至少有一个主进程
进程间:数据是无法共享的!!- 多线程编程:
线程(thread):CPU运行和资源调度的最小单位,微型的进程。在一个进程中,至少会有一个线程(可以更多),如果只有一个,这个线程就是主线程,线程之间是共享同一个进程间的数据的!!!- 协程编程
java官方没有提供协程开发,有些第三方的库和框架提供,非阻塞式IO、zero copy(零拷贝)
在了解多线程之前先要了解多任务,什么是多任务?即计算机同时运行多个软件或执行多个任务,在早期的单核CPU计算机情况下是使用时间片轮换和优先级别调度的方式来同时执行多任务,是一种伪多任务,因为一个CPU只能处理一个任务,为了能够同时处理多个任务,会在执行多任务时,给每个任务分配优先级,再处理,并时刻进行优先级比较轮换,由于切换执行另一个的时间很快,比人的反应时间还要快,所以看起来是同时处理多任务,实际上并不是多任务,直到多核CPU的出现才是真正的多任务。
而java是多线程实现多任务的!!!
java实现多任务使用的多线程
- 继承Thread类:
Thread类,该类是一个线程类,如果我们的自己的类继承了这个类,我们的类也就是一个线程。
步骤:
1、定义一个类,继承Thread类
2、重写run方法
注意:run方法就是线程需要执行的方法
3、创建子线程对象
4、使用start()方法启动子线程
在java的多线程中:
1.主线程的名称默认是main
2.子线程的名称默认是以Thread-N ,N是从0开始
public class TestThread01 {
public static void main(String[] args) {
// 如何启动一个子线程
// 创建一个子线程
MyThread mt = new MyThread("这个是子线程");
// 启动子线程
// 注意:使用start方法启动线程对象
// 一旦启动,除了主线程栈之外,会创建一个新的执行栈,开始执行代码
mt.start();
// 注意:一定不要使用run方法,run是需要你实现的,不是让你调用
mt.run();
for (int i = 0; i < 10000; i++) {
System.out.println(Thread.currentThread().getName() +"这个是主线程执行的代码——>"+ i);
}
}
}
// 该类就是一个线程类
class MyThread extends Thread {
// 重写run方法
@Override
public void run() {
// run方法就是线程方法
for (int i = 0; i < 10000; i++) {
// 已经继承了Thread,完全可以使用this关键字来替代Thread类名称
System.out.println(this.currentThread().getName() + "这个是一个子线程,它开始运行了——>"+ i);
}
}
public MyThread() {
}
public MyThread(String name) {
super(name);
}
}
- 实现Runable的接口
public class TestThread07 implements Runnable {
private int count = 0;
// private Object key = new Object();
@Override
public synchronized void run() {
for (int i = 0; i < 1000000; i++) {
synchronized (this) {
count++;
}
}
System.out.println("结果是:"+ count);
}
public static void main(String[] args) {
TestThread07 task = new TestThread07();
new Thread(task).start();
new Thread(task).start();
}
}
- 继承Thread和实现Runable接口的区别?
继承Thread类的子线程类,多个线程对象间是无法共享成员变量的!!!如果是静态成员,也是共享的!!!!
实现Runable接口都子线程类,多个线程对象间是共享线程类的成员变量的!!!
- Callable和Future接口(jdk1.5)
Callable接口需要一个泛型,该泛型指的是线程方法执行完成后,需要返回的结果的类型;
Future接口中的TaskFuture实现类
该实现类,已经实现了Future和Runable接口
public class TestThread03 {
public static void main(String[] args) {
// 创建线程对象 Callable线程对象
MyThread02 mt = new MyThread02();
// FutureTask 对象
FutureTask<String> future = new FutureTask<String>(mt);
// 启动了当前线程
new Thread(future, "call_thread").start();
for (int i = 0; i < 10000; i++) {
System.out.println(Thread.currentThread().getName() +"主线程开始运行了"+ i);
}
try {
// 注意:使用get方法获取,线程运行结束后的返回值
// 这个方法,意见在所有线程启动后,在调用
System.out.println("子线程运行后得到了结果是:"+ future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyThread02 implements Callable<String> {
// call方法就是线程方法
@Override
public String call() throws Exception {
for (int i = 0; i < 10000; i++) {
System.out.println(Thread.currentThread().getName()+ "线程开始运行了"+ i);
}
return "线程运行结束后的返回值";
}
}
- 线程对象的一些常见方法:
start() # 启动线程
run() # 该方法不能手动,是线程方法,start方法执行,JVM底层自动执行这个方法
setName(name) # 设置线程名称
getName() # 获取线程名称
getId() # 获取线程编号
getPriority() # 获取线程的优先级别
interrupt();
isAlive() # 判断线程是否存活
isDaemon() # 判断该线程是否是守护线程
setDaemon(true); # 将当前线程设置为守护线程
join(); # 阻塞主线程,让该子线程运行完成后再运行
Thread.sleep(毫秒) # 静态方法,当前线程休眠,会是否锁
Thread.yield() # 让当前线程放弃一次,不会释放锁
|-- 线程池(jdk1.5)
线程安全问题
在操作系统,因为线程之间是是共享同一个进程间的数据的,如果出现多线程,则有可能出现线程安全问题。就比如qq聊天不能共享数据,如果共享,那么每个聊天对象都知道你的聊天记录,玩全被他人偷听。在如下代码:
public class TestThread07 implements Runnable {
private int count = 0;
// private Object key = new Object();
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
{
count++;
}
}
System.out.println("结果是:"+ count);
}
public static void main(String[] args) {
TestThread07 task = new TestThread07();
new Thread(task).start();
new Thread(task).start();
}
}
结果:
可以看见结果发生了变化,标准结果应该都是2000000,但是却不同
原因是:
线程与线程之间抢优先级运行,线程与线程之间是相互不可见的,因此当一个线程进行自加时,值还没有赋值完,可能就会被另一个线程抢到了更高优先级而后运行,在另一个线程赋值完毕后,该线程会继续执行之前没完成的赋值操作,因为数据共享,导致之前赋的值作废。从而使结果千奇百怪。
如何解决线程安全
解决方案:加锁
-
java提供两种加锁方案:
- jdk1.0提供了,是一个关键字 synchronized(同步锁),范围尽量越小越好
有三种写法
1、放在方法上:整个方法都是同步
如果某个方法中所有代码,都有可能出现线程安全问题,建议将synchronized
直接写在方法上面,如果将synchronized写在方法上面,该方法就是锁
2、同步块:将有可能出现线程安全的代码放在一个同步块中
key就是一个对象,什么对象都可以,一般建议使用this关键字充当
synchronized (key) {
// 可能出现线程安全问题
count++;
}
3、静态方法:比较特殊,单独说
该静态方法加synchronized关键字,因为静态方法属于类,所以该类(本质就是该类的字节码文件)在充当锁。
public class TestThread07 implements Runnable {
private int count = 0;
@Override
public synchronized void run() {
for (int i = 0; i < 1000000; i++) {
synchronized (this) {
count++;
}
}
System.out.println("结果是:"+ count);
}
public static void main(String[] args) {
TestThread07 task = new TestThread07();
new Thread(task).start();
new Thread(task).start();
}
}
synchronized锁的锁升级问题
jdk7.0 synchronized进行了打的改动:
synchronized直接向操作系统申请锁,资源的消耗太大了,太重了。
Oracle进行了改造:
1、无锁状态(偏向锁)
2、锁就会从偏向锁升级到自旋锁(CAS)
CAS(compare and swap):比较并且交换
自旋锁很容易引起:ABA问题(如果在修改时候,有其他线程已经修改过这个值,但是恰巧其他线程的各种操作完成后,值又变回了之前的值,这样当前线程就没法感知这个值的变化情况,这就是著名的CAS的ABA问题,解决这个问题我们可以引入乐观锁的机制,增加一个版本号,也就是说这个变量每一次变化都增加一个版本号,这样就可以通过版本号的差异判断是否存在ABA)
3、自旋锁升级为重量级锁
-
synchronized在单例模式下的使用:
gof 23中设计模式:
|-- 装饰器设计模式
|-- 单例设计模式
构造方法,肯定私有化的。 -
饿汉式
直接将对象在属性上创建,static,直接被内存,程序不退出,内存是不释放
这种非常好,没有线程安全问题,唯一的缺陷就是不管使用不使用,内存都要占据,而且不会释放
public class Hungry{
private Hungry(){}
// 类加载的时候就实例化,并且创建单例对象
private static final Hungry hungry=new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
- 懒汉式
在需要使用对象的时候,再去创建对象,解决掉了饿汉式的内存占有问题。
懒汉式是无法直接使用的多线程中,因为非线程安全的.
public class Single2 {
// 1、起到了内存可见性;2、禁止指令重排序
private static volatile Single2 single = null;
// 私有化构造函数
private Single2() {}
// 懒汉式
public static Single2 getInstance() {
if (single == null) {
synchronized (Single.class) {
if (single == null) {
single = new Single2();
}
}
}
return single;
}
}
如果要解决线程安全问题,需要加锁处理:
|-- 直接在方法上面加锁,直接解决掉了线程安全,但是效率不高(锁的范围太大)
|-- 在创建对象的代码上加锁(锁的范围小,效率高)
|-- 如果这种写法,需要两次判断非空
- DCL(double check lock):
懒汉式,单例设计模式中的写法!!
要注意,指令重排序问题!!!
经典面试题:能不能去掉volatile关键字?
public class Single2 {
// 1、起到了内存可见性;2、禁止指令重排序
private static volatile Single2 single = null;
// 私有化构造函数
private Single2() {}
// 懒汉式
public static Single2 getInstance() {
if (single == null) {
synchronized (Single.class) {
if (single == null) {
single = new Single2();
}
}
}
return single;
}
}
答案是不能。
- jdk5.0提供了,Lock接口
Lock接口的使用:
jdk5.0时候,jdk提供了Lock接口,为了是改善synchronized重量级锁,而设计的轻量级的锁
lock() // 加锁
unlock() // 解锁
注意:
Lock加锁后,一定要保证能够解锁,否则有可能形成死锁。
public class TestThread02 {
public static void main(String[] args) {
MyThread mt = new MyThread();
new Thread(mt).start();
new Thread(mt).start();
}
}
class MyThread implements Runnable {
private Lock lock = new ReentrantLock();
private int count;
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
try {
// 在这儿加锁
lock.lock();
count++;
} finally {
// 必须要解锁
// 解锁的代码,要保证它必须执行
// 因此建议将解锁代码放在finally语句中!!!
lock.unlock();
}
}
System.out.println(count);
}
}
-
volatile关键字:
多线程情况下,每一个线程都独立拥有一个执行栈,每一个线程都是独立有用数据的,彼此之间是内存不可见的。
在java中,volatile关键字有两大核心作用:
1、被它修饰的变量,在多线程中,可以打破内存屏障,也就是说被它修饰的变量,就有可见性的。
2、禁止指令重排序(在一些情况下,jvm误以为改变指令的顺序不会产生影响,但实际情况不同)
public class TestThread03 {
public static void main(String[] args) throws InterruptedException {
MyThread03 mt = new MyThread03();
mt.start();
Thread.sleep(3000);
mt.flag = true;
System.out.println("此时count ="+ mt.count);
}
}
class MyThread03 extends Thread {
public int count;
// volatile 关键字,让变量可见性
public volatile boolean flag;
@Override
public void run() {
// 程序并没有被终止
while (!flag) {
count++;
}
}
}
- CAS和乐观锁及悲观锁:
乐观锁的概念:始终任务线程不会产生并发问题
悲观锁:只要并发,就会产生线程安全问题,解决了并发问题,但是效率较差
乐观锁:不加锁,给数据添加一个版本号,每当值发生变化,就对版本号加1,最终通过比较版本,判断是否并发 - 高并发:可见性、有序性、原子性
- 线程的生命周期:
每一个线程对象都是具备生命周期
死锁(dead lock):
多线程情况下,线程安全问题,通过加锁来解决问题。
死锁现象:一定要避免!!!造成大量的资料浪费,同时有解决不了问题。
避免死锁:一种比较优秀的解决方案:银行家算法
形成死锁有四个必要条件:
|-- 互斥
|-- 请求保持
|-- 环路等待
|-- 不可剥夺条件
死锁是一种资源的浪费,在正常的编程中,一定要避免死锁
- 死锁面试题
- 什么是死锁?
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:
- 产生死锁的原因?
- a. 竞争资源
系统中的资源可以分为两类:
1.可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源;
2.另一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
3.产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁 - b. 进程间推进顺序非法
1.若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁
2.例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁
- a. 竞争资源
- 死锁产生的4个必要条件?
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
- 解决死锁的基本方法
- 一、预防死锁:
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
- 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
- 一、预防死锁:
- 二、避免死锁:
- 预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
- 银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。
- 三、检测死锁
- 首先为每个进程和每个资源指定一个唯一的号码;
- 然后建立资源分配表和进程等待表。
- 四、解除死锁:
- 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
- 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
- 产生死锁的原因?