文章目录
笔者对 Java 的学习目前并未深入,很多东西笔者也没有弄的很懂,所以如果有错误的话,烦请各位指出。
一、多线程
在过去的学习中,我们知道了 main 方法执行方式,就是一行代码一行代码这样持续下去,直到结束为止。但是在很多情况下,这种情况不符合我们的使用需求。比如,我们知道服务器和客户端,是分为两个部分执行的,但是客户端一定有很多而服务器不一定。如果只有一个 main 方法,那么 Java 不能做到一个服务器响应多个客户端。此时多线程的引入就十分有必要。
1. 线程与进程
线程和进程是很容易被混淆的两个概念,但是二者却又本质的不同,所以我先来简单说说线程和进程。
1)进程
你可以想象一下,当你在电脑上打开了浏览器,那么此时,运行浏览器就是一个进程。又或者,你打开了游戏,打开了 steam 客户端,打开了暴雪战网……这些都是进程。通俗易懂(拿捏了!)。
2)线程
线程的理解比起进程稍微复杂那么一点点,就拿上面说的浏览器来说吧。当你打开了浏览器,搜索 “欧美大片” ,你就开启了一个新的线程,那么你再次搜索 “变形金刚” ,你就又开启了一个新的线程。虽然比起进程稍微有些复杂,但也不是很难,我相信各位大佬已经动了。当然真实情况肯定开启的线程不止这么点,这里就比喻一下,各位别当真哦。
2. 同步与异步
这个概念也很简单,没啥好唠叨的,我这就解释一波。
1)同步
排队进行,效率不高但是安全。
就看上面这么一句话可能很难想象我为什么说简单,那么我们老规矩,来举个栗子:假如你是个学生,你们班有个大佬学生买了一套《Java从入门到放弃》来学习,这不太好,你们都想看。然后这位大佬想了个办法,排队来,今天一号看,看完还给他,然后二号再看。不管一号看多久,反正二号得等着,不管一号看的是第几本,反正二号不能拿。这样就保证了书的位置大佬很清楚,很容易就能找到。
同步就是这么个意思啦。
2)异步
相信大家看完我同步的栗子,已经知道异步是咋回事了。咳咳,但是吧,毕竟我是在做博客,不说不太好。
同样是上面的栗子,现在大佬换了个方式,只要有人来借书,大佬就直接给。今天一号借走了第一部,二号借走了第二部,明天三号又来借走第四部,后天二号刚还的第二部就被四号借走了……这样做的方式效率可谓是极其高哇,大家都有书看。但是弊端也显而易见,没人知道现在大佬那里还有哪些书,大佬自己可能也搞混了借给哪些人了。
3. 并发与并行
相比较于同步和异步,并发与并行可难搞多了,隔段时间不看就很容易混,听我给给我细细道来。
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生。
有些许的绕,我给各位看官解释一下。并发就好比一个马路口,绿灯亮了,然后积攒的人潮哗哗哗地就开始走了,假如绿灯亮了 15 秒,那么我们就可以说,这个马路口 15 秒的并发量超级大!(虽然现实里没谁闲的没事干这么搞吧)
并行就比并发好搞多了,同样是马路口,这次我们来说车。假如你拿了个相机,路过了一个路口。此时正是红灯,马路上的车辆呼啸而过,你心血来潮,拿起相机对着对面就是一拍。那么在这一瞬间,冲过这个马路口的车辆全部进入了你的照片。假如照片上有 13 辆车吧,那这个马路口这一瞬间的车辆并行量就是13.
4. Thread
现在我们已经了解了关于线程的几个基本概念,该是实操一把的时候了!
我们来创建一个类(虽然我还没有写这个东西,但是我默认大家都知道了奥,后面我再补上)
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("第" + i + "号大佬");
}
}
}
这就是 Thread 的使用方法第一步了,@Override 是重写方法的标记,run 是继承 Thead 类需要重写的方法,这里这个 run 方法就是一个线程了。那么我们要如何来启动它呢?
public class Main {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
for (int i = 0; i < 5; i++) {
System.out.println("第" + i + "号菜鸡");
}
}
}
很简单,在主线程 main 里面把 MyThread 对象 new 出来,然后跟上个 .start(); 就好啦,这个东西就是这个分线程的启动方法。那么我们来看看结果。
是不是很简单?接下来我们来看个更简单的。
public class Main {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("第" + i + "号菜鸡");
}
}
}.start();
for (int i = 0; i < 5; i++) {
System.out.println("第" + i + "号菜鸡");
}
}
}
这是匿名内部类的写发,也是我们新手学习期的常用写发。
5. Runnable
和 Thread 差不多,我们也可以这样来用,不过需要注意的是这是个接口。
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("第" + i + "号学霸");
}
}
}
然后主线程怎么写呢?这就上!
public class Main {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("第" + i + "号菜鸡");
}
}
}
这样看起来,似乎 Runnable 用起来更复杂一些啊,那么这个接口有什么有点呢?小的这就为您解答。
Runnable 在实际的使用中其实更为广泛。因为相比于 Thread,Runnable 有更多优点:
- 更适合多个线程同时执行相同任务的情况。
- 避免了单继承带来的局限性,毕竟 Java 只能单继承,但是可以多实现嘛。
- 任务和线程本身是分离的,提高了程序的健壮性。
- 线程池技术接受 Runnable 类型的任务,不接受 Thread 类型的任务。
5.线程相关
线程这方面有些有意思的玩法和一点小小的常识,我们来一起看一下。
1)线程休眠
就像名字说的,可以让线程暂停工作的方法,这个方法是 Thread 类下的一个静态方法,不需要创建 Thread对象就可以直接调用。
public class Main {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
System.out.println("第" + i + "号菜鸡");
Thread.sleep(1000);
}
}
}
需要注意的是,sleep 里的 1000 是指毫秒,这个程序可以让电脑每隔一秒钟输出一次。
2)线程阻塞
线程阻塞不是卡住才叫阻塞的,只要有一个操作让程序的执行时间大量消耗,那么就能叫做线程阻塞。
3)线程死锁
比如两个线程在交互时,两个都在等对方发消息。这就好像警察和罪犯一个说:“先放人质我们就放你。”一个说:“先放了我,我就放了人质。” 俩人就卡着事情就毫无进展。线程也是一样的,如果出现了这种情况,就形成了线程死锁,我们应当尽量避免这种情况发生。
6. 线程安全问题
前面已经说了线程存在安全与不安全的问题,那么我们怎么解决它呢?
我们来看看不安全的案例
这是分线程类
public class MyRunnable implements Runnable{
private Integer num = 10;
@Override
public void run() {
while (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num--);
}
}
}
这是主线程
public class Main {
public static void main(String[] args) throws InterruptedException {
MyRunnable a = new MyRunnable();
new Thread(a).start();
new Thread(a).start();
new Thread(a).start();
new Thread(a).start();
}
}
我们来看看输出结果,看看他为什么不安全。
可以看到,我们在循环时的条件是 num > 0 才输出,但是居然出现了 -1,-2 的情况。这是为什么呢?
这是因为运行时线程休眠的原因,当 num = 1 的时候,1 线程走到了等待 100 毫秒那里,然后等待的这段时间里 2,3 线程也进来了,这时候1线程把 num 改成0,于是 4 线程进不来了,但是 2 ,3 线程已经进来了鸭,进来就得走完这一程,于是 num 接着连续减了 2 次,变成了 -2 。
1)同步代码块
上面我们看到的这个问题怎么来解决呢?我们可以给它上个锁。
public class MyRunnable implements Runnable{
private Integer num = 10;
private Object o = new Object();
@Override
public void run() {
synchronized (o) {
while (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num--);
}
}
}
}
主线程程序不变,方便阅读我我再给大家展示一下。
public class Main {
public static void main(String[] args) throws InterruptedException {
MyRunnable a = new MyRunnable();
new Thread(a).start();
new Thread(a).start();
new Thread(a).start();
new Thread(a).start();
}
}
我们来看输出结果
可以看到现在已经没有输出负数的情况了,那么这是个啥原理呢?看图。
只要锁内部还有线程在执行,其余线程都得排队等待。注意 Object 对象不能在 run 方法内部创建,否则就是每个线程都有一把锁,大家都看自己的锁有没被锁上,那不是白搞了嘛。
2)同步方法
和同步代码块类似,只是这一次,我们把锁加到了方法上。
public class MyRunnable implements Runnable {
private Integer num = 10;
@Override
public void run() {
while (true) {
if (!number())
break;
}
}
public synchronized boolean number() {
if (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num--);
return true;
}
return false;
}
}
来看运行结果
这样也可以避免出现负数。
3)显式锁 Lock
显式锁的用法也非常简单,我们一起来看一下
public class MyRunnable implements Runnable {
private Integer num = 10;
private Lock l = new ReentrantLock();
@Override
public void run() {
l.lock();
while (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num--);
}
l.unlock();
}
}
老规矩,来看看运行结果,看他锁住没。
锁没锁住还是显而易见的哈。