多线程
多线程
概述:为了解决单线程程序的效率问题
多线程相当于 好几个人 一起抢着干一个任务,提高了效率,但是有安全隐患
面试题:
进程与线程的区别
进程是正在运行的程序 一个程序的运行可能依赖多个进程
线程被包含在进程中 是进程中的实际运作单位
一个进程包括多个线程
并行和并发的区别
并行是指 同一个时刻没有人抢
并发是指 同一个时刻,多人抢占,共享资源
线程状态
1. 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3. 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4. 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- (01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
- (02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
- (03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
start()和run()的区别
start()才是真正的启动线程, run()只是一个普通的方法调用
start()能够发生多线程的随机性抢占资源的特性, run()只是顺序执行
特点
随机性 是指 程序的执行权,只能交给CPU调度.但是CPU的执行我们无法干预.
比较 --extends Thread 好处: 方法很多,方便调用 坏处: 占用了唯一的 继承机会 --implements Runnable 好处: 实现了这个接口,还可以去实现其他接口,还可以再继承 坏处: 方法调用麻烦,代码体现的比较麻烦... --Thread.currentThread()--获取当前正在执行任务的线程对象 --new Thread(接口实现类的对象).start();

测试方法
/*
--1,继承Thread
--创建对象
Thread()
Thread(Runnable target)
Thread(Runnable target, String name)
--方法
static Thread currentThread()
返回对当前正在执行的线程对象的引用。
long getId()
String getName()
void setName(String name)
void run()
static void sleep(long millis)
void start()
void stop()
*/
public class C1{
public static void main(String[] args) {
//创建对象
Thread t = new Thread();
//调用方法
System.out.println(t.getId());//获取线程的标识
t.setName("么么哒");//设置线程名
System.out.println(t.getName());//获取线程名
t.run();
t.start();//使线程开始执行
t.stop();//结束线程
Thread now = new Thread();//获取当前正在执行任务的线程对象
System.out.println(now.getName());
System.out.println(now.getId());
}
}
模拟多线程
public class C1{
public static void main(String[] args) {
/*
for (int i = 0; i < 10; i++) {
new MyThread().start();
}
*/
MyThread t = new MyThread();
MyThread t2 = new MyThread();
//t.run();//简单方法调用,没有多线程的效果
t.start();//启动线程 变成可运行状态
t2.start();
/* 6,多线程程序运行结果的随机性,,多个线程抢占执行权
Thread-0~~0
Thread-1~~0
Thread-0~~1
Thread-1~~1
Thread-0~~2
Thread-0~~3
Thread-1~~2
Thread-0~~4
*/
}
}
//1,自定义多线程程序
class MyThread extends Thread{
//2,多线程编程里,要求把业务 放在重写 run()
//generate-override methods-选中-ok
@Override
public void run() {//开始执行run()--运行状态
//需求:打印10次线程名称
for (int i = 0; i < 10; i++) {
//3,调用父类的方法获取线程名称
System.out.println(super.getName()+"--"+i);
}//run()执行完变成 终止状态
}
}
实现Runnable接口 //测试 多线程编程 -- 方式2::: 实现Runnable接 public static void main(String[] args) {
//5, 创建对象测试
MyRunnable tareget = new MyRunnable();
//TODO MyRunnable 和 Thread 绑定关系
Thread t = new Thread(tareget);
Thread t2 = new Thread(tareget);
t.start();//6, 开启线程
t2.start();
}
}
//1, 自定义多线程程序-- 方式2::: 实现Runnable接口
class MyRunnable implements Runnable{
//2, 实现了接口,实现类必须 重写 所有的抽象方法,否则就是一个抽象类
//3, 多线程编程里,需要把业务放在 重写的run()里
@Override
public void run() {
//需求:打印10次线程名称
for (int i = 0; i < 10; i++) {
//4, Thread.currentThread()获取正在执行任务的线程
//getName()-获取正在执行任务的线程的名字
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}
启动线程方式:
1.第一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法,
然后在run方法里填写相应的逻辑代码
2.第二种方法是实现Runnable接口,并编写run方法,相比继承Thread类创建线程的好处
是以实现接口的方式创建线程可以对类进行更好的扩展,该类可以继承其他类来扩展自身需
求,相比第一种方式更加灵活,扩展性强。
3.实现Callable接口创建线程与Runnable接口的不同之处在于:如果你想要在
线程执行完毕之后得到带有返回值的线程则实现Callable接口。
关闭线程的方式:
1. 使用标志位终止线程
在 run() 方法执行完毕后,该线程就终止了。但是在某些特殊的情况下,run() 方法会被一直执行;比如在服务端程序中可能会使用 while(true) { ... } 这样的循环结构来不断的接收来自客户端的请求。此时就可以用修改标志位的方式来结束 run() 方法。
1. 使用标志位终止线程
在 run() 方法执行完毕后,该线程就终止了。但是在某些特殊的情况下,run() 方法会被一直执行;比如在服务端程序中可能会使用 while(true) { ... } 这样的循环结构来不断的接收来自客户端的请求。此时就可以用修改标志位的方式来结束 run() 方法。
2. 使用 stop() 终止线程
通过查看 JDK 的 API,我们会看到 java.lang.Thread 类型提供了一系列的方法如 start()、stop()、resume()、suspend()、destory()等方法来管理线程。但是除了 start() 之外,其它几个方法都被声名为已过时(deprecated)。
3. 使用 interrupt() 中断线程
现在我们知道了使用 stop() 方式停止线程是非常不安全的方式,那么我们应该使用什么方法来停止线程呢?答案就是使用 interrupt() 方法来中断线程。
需要明确的一点的是:interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。
也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。这一点很重要,如果中断后,线程立即无条件退出,那么我们又会遇到 stop() 方法的老问题。
多线程编程中的三个核心概念
原子性
这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。
关于原子性,一个非常经典的例子就是银行转账问题:比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。
可见性
可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。可见性问题是好多人忽略或者理解错误的一点。
顺序性
顺序性指的是,程序执行的顺序按照代码的先后顺序执行。
多线程高并发问题
锁和同步方法(代码块)
使用锁,可以保证同一时间只有一个线程能拿到锁,也就保证了同一时间只有一个线程能执行申请锁和释放锁之间的代码。
Java如何保证可见性
Java提供了volatile关键字来保证可见性。当使用volatile修饰某个变量时,它会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值。
各种锁
互斥量(锁):用于保护关键的代码段,以确保其独占式的访问。
1. 乐观锁/悲观锁
这两个概念是人们对java中各种锁总结提出的模型,不是特指某种类型的锁。 乐观锁预期数据的并发操作不会发生修改而不需要进行加锁的操作,悲观锁则相反。在java中的乐观锁一般采用CAS算法或者版本控制,典型的应用如原子类操作。悲观锁则应用的比较广泛如Synchronized等等。
综上:
乐观锁适用于读操作比较多的场景,
悲观锁适用于写操作比较多的场景。
2.独享锁/共享锁
独享锁也称独占锁,是指该锁每次只能被一个线程占有,共享锁则可以被多个线程使用。
例如ReentrantReadWriteLock里的读锁是共享的,但是它的写锁是独享的。锁的共享可保证并发读是非常高效的,但是读写和写写,写读都是互斥的。
3.互斥锁/读写锁
互斥锁:
互斥意味着一个锁某一时刻只能被一个线程持有,其它试图获取锁的线程都会被阻塞,直至当前锁释放,该锁上的其它线程进入就绪状态,准备抢占锁,例如synchronized, 在jdk1.5版本之后又出现了Lock锁,它提供了比synchronized机制更加广泛的锁操作,后续将会单独开篇文章详细介绍这两种锁
读写锁:
包含了上文提到的独享锁/共享锁,写既是独享锁,读是共享锁,读远远大于写的场景非常需要用到它。相比于传统的锁,读读是不互斥的只有涉及到写才会互斥,这样就比传统锁提高了cpu资源利用率,可以说读写锁就是为了优化这种场景而设计的。
浙公网安备 33010602011771号