java高级之多线程
1.什么是多线程
首先引入程序与进程概念:
-
程序(program)
程序是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码(还没有运行起来),静态对象。
-
进程(process)
进程是程序的一次执行过程,也就是说程序运行起来了,加载到了内存中,并占用了cpu的资源。这是一个动态的过程:有自身的产生、存在和消亡的过程,这也是进程的生命周期。
进程是系统资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
-
线程(thread)
进程可进一步细化为线程,是一个程序内部的执行路径:线程是以cpu调度分配的单位。
若一个进程同一时间并行执行多个线程,那么这个进程就是支持多线程的。
【当然,一个进程最少包括一个线程,要不然无意义】
2.线程创建的几种方法
2.1创建方式一:继承Thread
public class MyThread extends Thread{
}
继承Thread重写run方法:
public class MyThread extends Thread{
public void run(){
}
}
这个run,当主线程运行时就会执行run里面的程序【注:main方法就是主线程】
这里举例:
public class MyThread extends Thread{
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println("我是run线程=="+i);
}
}
}
在run中我写上for循环,以便一会演示而用。
--------
新建测试类并声明main方法
随后创建MyThread类对象
并调用start()方法启用run线程
例:
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
我在main方法中同样写上一个for循环
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("我是main线程=="+i);
}
}
此时运行,run线程与主线程同为竞争管理
因为他们会同时抢夺cpu调度资源
至于哪个线程抢的多哪个线程抢的少就全凭运气
运行结果:
确实随机...
2.1.1 线程的命名和获取
首先是获取当前线程名称:
getName();
System.out.println(this.getName()+"我是run线程=="+i);
这里可以直接使用this用来指代当前new对象
当然,如果你不先给线程设置名称的话就会使用默认名称,以 Thread-? 的形式
这里再次使用 setName() 来为线程命名
MyThread myThread = new MyThread();
myThread.setName("线程A-");//为线程命名
myThread.start();
再次运行:
当然,getName() 虽说好用,但是他有缺陷
他只能被用于继承Thread的子类身上
你把 getName() 用在main身上就不行了
所以推出另一种万能获取线程名称的方法
Thread.currentThread().getName()
这个不管是什么牛马,都可Thread加身,非常的牛13
System.out.println(Thread.currentThread().getName()+"我是main线程=="+i);
这里直接加在main中,效果可见:
2.2 创建方式二:实现 Runnable 接口
public class MyThread2 implements Runnable{
}
public class MyThread2 implements Runnable{
public void run() {
}
}
我还是在其中写上一个for循环
public class MyThread2 implements Runnable{
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我是run线程=="+i);
}
}
}
在main方法中同样写上for循环
现在的话不单单是直接new MyThread2就能行的
还得new Thread才阔以,并将对象传入Thread中,同时还可以有第二个参数,可以直接为此线程命名
MyThread2 myThread2 = new MyThread2();
Thread thread = new Thread(myThread2,"窗口1");
剩下的直接运行,与第一种方法相同
优缺点:
- 第一种确实方便,但是不利于效率,因为只能继承一个,也不能重写什么接口了,限制很大
- 第二种相对于麻烦了一步,但是它可以同时继承,同时重写多个接口
2.3创建方式三:Callable创建线程(实现接口)
创建一个线程类,并实现Callable的call方法,这里类名称就用ThreadCall指代
与前两种创建方式不同,但是又和Runnable实现的创建方式相似,这个更可以看成Runnable的进阶版
与Runnable不同的是,Callable不仅有泛型,还有返回值,并且实现的方法也不同此处实现的是call方法
public class ThreadCall implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return null;
}
}
创建一个测试类
这里有些许不一样,我都感觉薛薇有点麻烦,但是不要紧,后面会讲到线程池,到时候就直接用线程池创建,也是很方便的
public static void main(String[] args) {
ThreadCall threadCall = new ThreadCall();
FutureTask<Integer> integerFutureTask = new FutureTask<>(threadCall);
Thread thread = new Thread(integerFutureTask);
thread.start();
}
反正就是隔这套娃呢!!!
3.线程卖票例子
3.1继承Thread方式卖票
新建MyThread3并继承Thread重写run方法
并定义变量ticket票数等于100
public class MyThread3 extends Thread{
private int ticket = 100;
@Override
public void run() {
while (ticket > 0){
ticket--;
System.out.println(Thread.currentThread().getName()+"卖了1张票,还剩"+ticket+"票");
}
}
}
再次新建测试类并什么main方法中创建三个窗口同时卖票,【注意,是三个窗口同时各卖100张票】,创建三个窗口也就意味着是创建三个线程
public static void main(String[] args) {
MyThread3 myThread3 = new MyThread3();
myThread3.setName("窗口一");
myThread3.start();
MyThread3 myThread4 = new MyThread3();
myThread4.setName("窗口二");
myThread4.start();
MyThread3 myThread5 = new MyThread3();
myThread5.setName("窗口三");
myThread5.start();
}
运行测试:
太多了,太多了,太多了这里盛不下省略..................................
3.2 实现接口Runnable 方式卖票
新建MyThread4实现 Runnable 接口并重写run方法,
并定义变量ticket票数等于100
public class MyThread4 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (ticket > 0){
ticket--;
System.out.println(Thread.currentThread().getName()+"卖了1张票,还剩"+ticket+"票");
}
}
}
新建测试类文件并声明main方法中创建三个窗口同时卖100张票,没错,你没看错,是三个窗口同时卖同100张票
public static void main(String[] args) {
MyThread4 myThread4 = new MyThread4();
Thread thread = new Thread(myThread4,"窗口1");
thread.start();
Thread thread2 = new Thread(myThread4,"窗口2");
thread2.start();
Thread thread3 = new Thread(myThread4,"窗口3");
thread3.start();
}
运行测试:
你会发现问题的
无论是继承Thread的方式卖票,还是重写 Runnable 方法的方式卖票,结果都有一个共同点,三个窗口会重复卖掉同一张票,并且会使得库存呈现负数,这是极其不合理的
所以,这里就开始涉及到 线程安全 的方向
4.线程安全--锁的概念
4.1自动锁 synchronized(共享资源){}
这一概念用大白话来说就是一间厕所只有一个坑位,但是这个时候却有三个人要嘘嘘,当然不可能三个人同时对着坑位嘘嘘,这个时候某一个人率先出手抢到了坑位,然后给厕所门一锁
另外两人只能等在门后,待某人解决后开门,临到下一人,继续关门上锁嘘嘘,循环往复,自动锁的概念就此简单化
当然,在上锁之前,我们的代码需要简单改动一下在上锁
public class MyThread4 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true){
synchronized(this){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket--;
System.out.println(Thread.currentThread().getName()+"卖了1张票,还剩"+ticket+"票");
}
else {
break;
}
}
}
}
}
如果你在测试的时候发现效果不明显,那就加上 Thread.sleep(100); 这个方法,效果杠杠的
至于什么作用,稍后说道说道
4.2 手动锁 lock
演示,效果跟 那个自动锁一样,只是变成了手动
首先就是new 一个 ReentrantLock();
public static ReentrantLock lock = new ReentrantLock();
public class MyThread4 implements Runnable{
private int ticket = 100;
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock();//手动开启锁
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket--;
System.out.println(Thread.currentThread().getName()+"卖了1张票,还剩"+ticket+"票");
}
else {
break;
}
lock.unlock();//手动关闭锁
}
}
}
测试运行:效果奇好
这样大大有利于线程安全的漏洞
5.Thread类的常用方法
- start() : 启动当前线程, 调用当前线程的run()方法
- run() : 通常需要重写Thread类中的此方法, 将创建的线程要执行的操作声明在此方法中
- currentThread() : 静态方法, 返回当前代码执行的线程
- getName() : 获取当前线程的名字
- setName() : 设置当前线程的名字
- yield() : 释放当前CPU的执行权
- join() : 在线程a中调用线程b的join(), 此时线程a进入阻塞状态, 知道线程b完全执行完以后, 线程a才结束阻塞状态
- stop() : 已过时. 当执行此方法时,强制结束当前线程.
- sleep(long militime) : 让线程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态
这里方法太多,就不一一测试了,感兴趣的小伙伴可以尝试着调用其中的方法,看看是什么效果
6.线程死锁
线程死锁,怎么理解,一个画板是需要一根画笔和颜料组成才能画画,张三把画笔拿走了,李四把颜料拿走了,李四想画画,但是定睛一瞅,张三去厕所了,完了,画不成了,等等等等等好久没见张三出来,一听在厕所便秘,WC,真画不成了。如果张三一直便秘,就一直画不了画,等价与程序无响应,等价于线程死锁
6.1利用代码实现线程死锁
声明一个锁对象,这里暂代用MyThread6来作为
public class MyThread6 {
public static Object lockA = new Object() ;
public static Object lockB = new Object();
}
为了方便演示,这里不在一 一建立Thread类
直接使用匿名创建的方式演示:
如下:
在测试类文件中声明main方法,并创建两个 线程,在其中声明匿名 new Runnable 实现run方法
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (MyThread6.lockA){
System.out.println("张三获取到了锁A");
synchronized (MyThread6.lockB){
System.out.println("张三获取到了锁B");
System.out.println("可以绘画了");
}
}
}
});
thread.start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (MyThread6.lockB){
System.out.println("李四获取到了锁B");
synchronized (MyThread6.lockA){
System.out.println("李四获取到了锁A");
System.out.println("可以绘画了");
}
}
}
});
thread2.start();
}
6.2 运行测试
当运行多次后,只会得到两种结果,一种就是直接出现死锁情况,一种是未锁
7.线程通信
【有一种业务需求,是需要功能一边相加,另一边相减,比值相等】
线程通信利用同一种共享资源进行增加和减少,利用 wait(); notify();
wait(); //指代线程无期限休眠,且释放CPU资源,通过notify() 来唤醒其休眠的状态,继续竞争CPU资源的抢夺,如果没有唤醒该线程,将会永无休止的沉眠
notify(); //指代唤醒wait(),且是随机唤醒等待队列中一个线程
7.1银行取钱/存钱
这里将会通过银行的存钱和取钱的方式来演示线程通信的使用
【注:这里的银行取钱存钱指代,张三与李四共享一张银行卡,共享余额,
这里会有要求:
1.当银行卡有钱时李四必须将钱取出来,当银行卡没钱时李四必须等待
2.当银行卡没钱时张三必须将钱存入,当银行卡有钱时,张三必须等待】
- 创建银行卡类
并声明余额、与银行卡状态两个属性
public class BankCard {
private int balane;//余额
private boolean flag;//状态:当flag = true,代表银行卡有钱,当flag = false,代表银行卡没钱
}
2.创建两个方法:存钱方法、取钱方法
并给其 方法上锁
public class BankCard {
private int balane;
private boolean flag;
//存钱方法
public synchronized void save(int money) throws InterruptedException {
}
//取钱方法
public synchronized void take(int money) throws InterruptedException {
}
}
3.依次按照要求
public class BankCard {
private int balane;//余额
private boolean flag;//银行卡状态
public synchronized void save(int money) throws InterruptedException {
if(flag == true){//当银行卡有钱时
this.wait();//线程休眠,等待唤醒
}
balane += money;//否则就是存钱
System.out.println(Thread.currentThread().getName()+"往卡中存了"+money+"元; 余额剩:"+balane);
flag = true;//代表银行卡有钱
this.notify();//唤醒正在休眠的线程,这里是指代当前,即银行卡没钱的时的休眠线程
}
public synchronized void take(int money) throws InterruptedException {
if (flag == false){//银行卡没钱时
this.wait();//线程休眠
}
balane -= money;//取钱
System.out.println(Thread.currentThread().getName()+"从卡中取了"+money+"元; 余额剩:"+balane);
flag = false;//取钱后银行卡没钱
this.notify();//唤醒正在休眠的线程,指代银行卡有钱时休眠的线程
}
}
4.声明两个线程类,这里使用实现Runnable的方式
这里Thread1代表张三存钱线程
Thread2代表李四取钱线程
public class Thread1 implements Runnable{
/**
* 张三存钱
*/
private static int money;
public static BankCard bankCard;
public Thread1(BankCard b){
bankCard = b;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
bankCard.save(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Thread2 implements Runnable{
/**
* 李四取钱
*/
public static BankCard bankCard;
public Thread2(BankCard b){
bankCard = b;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
bankCard.take(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
5.新建测试类
public class test {
public static void main(String[] args) {
BankCard bankCard = new BankCard();
Thread1 saves = new Thread1(bankCard);
Thread2 takes = new Thread2(bankCard);
Thread thread = new Thread(saves,"张三");//张三存钱
Thread thread2 = new Thread(takes,"李四");//李四存钱
thread.start();
thread2.start();
}
}
6.测试运行
【注:这里是循环10次,每次存入/取出 1000元】
8.线程状态
- NEW : 新建状态
- RUNNABLE : start()就绪状态-时间片-运行状态. 统称为RUNNABLE
- BLOCKED : 堵塞状态。加锁时就如该状态
- WAITING : 无期等待: 调用wait方法时会进入该状态
- TIMED_WAITING : 有期等待---当调用sleep方法时就会进入该状态
- TERMINATED :终止状态。线程的任务代码执行完毕或出现异常。
【注:线程的状态之间可以通过调用相应的方法,进行转换】
9.线程池
线程池,线程池的出现可以大大提高线程的利用效率,怎么说?
假如有5个线程池,这时却有7个任务,那多出来两个怎么办?
好办,继续调用线程池已释放资源的线程,循环往复
理论说只要释放够快,可以一直这样循环下去
先了解创建线程池的几种状态
9.1创建线程池的几种状态
1.创建一个固定长度的线程池。
ExecutorService executorService = Executors.newFixedThreadPool(?个线程);
2. 单一线程池。
ExecutorService executorService = Executors.newSingleThreadExecutor();
3. 可变线程池--缓存线程池
ExecutorService executorService = Executors.newCachedThreadPool();
4. 延迟线程池。
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
9.2线程池的创建
新建测试类,并声明main方法
这里使用固定长度的线程池的方式创建
且使用匿名实现Runnable的方式创建线程
且使用for循环模拟多任务少线程的方式的情景,看看线程池如何循环往复
public static void main(String[] args) {
//线程池的创建
//使用创建固定长度的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
/**
* 这里为了方便演示
* 使用匿名实现Runnable的方式
* 并使用for循环模拟6个任务需要5个线程的情景
*/
for (int i = 0; i < 6; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
测试运行
结果很明显,当线程小于任务数时,线程池这时就会调用已释放资源的线程,很明显,线程1先释放,自然被线程池优先调用
【注:以上通过Executors工具类创建线程池,但是阿里巴巴不建议使用。阿里建议使用原生的模式创建线程池】
9.3原生模式创建线程池
在这之前咱们先了解一下原生模式创建线程池的参数
int corePoolSize :核心线程的个数
int maximumPoolSize :最多的线程个数
long keepAliveTime :线程空闲时间。
TimeUnit unit : 空闲的单位
BlockingQueue<Runnable> workQueue :等待队列
创建测试类并依然声明main方法,使用原生线程池模式创建
public static void main(String[] args) {
//原生模式创建线程池
ArrayBlockingQueue<Runnable> runnables = new ArrayBlockingQueue<>(5);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 5, TimeUnit.SECONDS, runnables);
for (int i = 0; i < 10; i++) {//for循环模拟线程与任务数
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
---------
以上便是java高级之线程中的部分内容,如有漏缺请在下方留言告知,我会及时补充