Java并发编程之多线程 - 小简博客 (ideaopen.cn)https://blog.ideaopen.cn/index.php/archives/42/
我们首先,先要了解什么是进程,什么是线程。
首先,我们看看进程。我们如果允许一个程序,它卡死了,我们通常会去任务管理器里面将进程结束。
所以,这里所看见的,就是进程。
那么,何为线程呢?
首先,看看来自知乎的解释:
线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
啥意思呢?通俗的说。QQ
和 Chrome
浏览器是两个进程,Chrome
进程里面有很多线程,例如 HTTP
请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件,同时你开多个窗口浏览网页也没问题。
想了解得更详细点,可以看看知乎这一篇——>进程和线程
然后,腾讯云还有一篇,链接中文,不好分享,我直接截图。
多线程优势
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
优势很多,我就直接去网上搬运来了,能达到学会的目的就可以,不在意是不是自己描述出来。
一、多线程优势
采用多线程技术的应用程序可以更好地利用系统资源。主要优势在于充分利用了CPU的空闲时间片,用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。
线程同步,在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。在Visual Basic中提供了三种方法来完成线程的同步。在Java中可用
synchronized
关键字。二、代码域同步
使用
Monitor
类可以同步静态/实例化的方法的全部代码或者部分代码段。三、手工同步
可以使用不同的同步类创建自己的同步机制。这种同步方式要求你自己手动的为不同的域和方法同步,这种同步方式也可以用于进程间的同步和解除由于对共享资源的等待而造成的死锁。
四、上下文同步
使用
SynchronizationAttribute
为ContextBoundObject
对象创建简单的,自动同步。这种同步方式仅用于实例化的方法和域的同步。所有在同一个上下文域的对象共享同一个锁。总结多线程的好处,使用线程可以把占据时间长的程序中的任务放到后台去处理;用户界面更加吸引人,这样比如用户点击了一个按钮去触发某件事件的处理,可以弹出一个进度条来显示处理的进度;程序的运行效率可能会提高;在一些等待的任务实现上如用户输入,文件读取和网络收发数据等,线程就比较有用了。
以上很多部分,如果看不懂,可能是许多东西还未涉及到。
实现多线程
我们实现Java的多线程呢,有4中方法。
1.继承Thread类创建线程
2.实现Runnable接口创建线程
3.实现Callable接口通过FutureTask
包装器来创建Thread线程
4.使用ExecutorService
、Callable
、Future
实现有返回结果的线程(线程池方式)
多线程如何运行?
菜鸟教程其他的不行,图还是好用,我们看看这张图。
如果正常运行的话,路径就是这样的:
新建线程——>就绪状态——>运行状态——>死亡状态
Thread类创建线程
使用Thread
类创建线程,我们首先需要继承它,并且重写run
方法。
满足这两个条件就可以。
我这里,为了体现多线程的并发,我使用了Time
下的LocalTime
类,来体现时间的变化。
sleep()
方法可以设置延迟,也就是说,运行一次后,我这里需要1000
毫秒在运行下一次,我加个循环运行一下。
try {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.out.println(this.getName()+"多线程输出"+LocalTime.now());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
在创建一个入口类,运行起来。
package Thread;
public class ThreadTest {
public static void main(String[] args) {
ThreadDemo t = new ThreadDemo();
t.start();
ThreadDemo t1 = new ThreadDemo();
t1.start();
ThreadDemo t2 = new ThreadDemo();
t2.start();
}
}
这里创建了三个线程,使用start()
方法可以运行起来,调用run
方法。
注意!!!不是.run()
,是.start()
。
运行下了,结果如图。
Thread-2多线程输出18:29:22.345
Thread-1多线程输出18:29:22.345
Thread-0多线程输出18:29:22.345
//sleep(1000),延迟1s后继续运行
Thread-1多线程输出18:29:23.356
Thread-2多线程输出18:29:23.356
Thread-0多线程输出18:29:23.356
//sleep(1000),延迟1s后继续运行
Thread-1多线程输出18:29:24.357
Thread-2多线程输出18:29:24.357
Thread-0多线程输出18:29:24.357
看看后面的时间,T
、T1
、T2
都是同时运行的,比如第一次,都是18:29:22.345
这个时间。
至于为啥三个对象顺序不一样,这就相当于挤公交,鬼知道谁先挤进去呢?嘿嘿。
Thread 方法
下表列出了Thread类的一些重要方法:
序号 | 方法描述 |
---|---|
1 | public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
2 | public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
3 | public final void setName(String name) 改变线程名称,使之与参数 name 相同。 |
4 | public final void setPriority(int priority) 更改线程的优先级。 |
5 | public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
6 | public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。 |
7 | public void interrupt() 中断线程。 |
8 | public final boolean isAlive() 测试线程是否处于活动状态。 |
上述方法是被 Thread 对象调用的,下面表格的方法是 Thread 类的静态方法。
序号 | 方法描述 |
---|---|
1 | public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。 |
2 | public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
3 | public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
4 | public static Thread currentThread() 返回对当前正在执行的线程对象的引用。 |
5 | public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。 |
方法用法都一样,请自行斟酌。
Runnable接口创建线程
因为和Thread
创建线程类似,我就直接放代码了。
package Runnable;
import java.time.LocalTime;
public class RunnableDemo implements Runnable {
@Override
public void run() {
//设置延迟
try {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"多线程输出"+ LocalTime.now());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这里也是两个条件。
implements Runnable
和@Override run
。继承接口,重写run
方法。
Thread.currentThread().getName()
这个是返回当前调用的主线程的名字。
运行类就有点不一样了。
package Runnable;
public class RunnableTest {
public static void main(String[] args) {
RunnableDemo r = new RunnableDemo();
Thread t = new Thread(r);
t.start();
RunnableDemo r1 = new RunnableDemo();
Thread t1 = new Thread(r1);
t1.start();
RunnableDemo r2 = new RunnableDemo();
Thread t2 = new Thread(r2);
t2.start();
}
}
//创建线程对象
RunnableDemo r = new RunnableDemo();
//将线程对象放在Thread类对象重
Thread t = new Thread(r);
//调用start方法
t.start();
运行结果也一样。
为何要用Runnable
那有人就说了,为啥这个东西多了一步,还麻烦,我怎么不直接用Thread
呢?
我们首先要明白,Java语言不可以多继承。
两者实现方式带来最明显的区别就是,由于Java不允许多继承,因此实现了Runnable接口可以再继承其他类,但是Thread明显不可以。
Runnable可以实现多个相同的程序代码的线程去共享同一个资源,而Thread并不是不可以,而是相比于Runnable来说,不太适合。
线程的调度
线程的调度的操作,有如下常用方法。
方法 | 作用 |
---|---|
int getPriority() | 返回线程的优先级 |
void setPrority(int newPrority) | 更改线程的优先级 |
boolean isAlive() | 判断线程是否处于活动状态 |
void join() | 使进程中其他线程等待该线程终止后再运行 |
void yield() | 暂停当前正在执行的线程对象并允许其他线程 |
线程优先级
RunnableDemo r = new RunnableDemo();
Thread t = new Thread(r);
//设置优先级
t.setPriority(Thread.MAX_PRIORITY);
t.start();
这样就可以设置线程对象优先级,优先级有三个常量。
MIN_PRIORITY //值为1 最低
NORM_PRIORITY //值为5 普通级别
MAX_PRIORITY //值为10 最高
线程强制
package Runnable;
public class RunnableTest {
public static void main(String[] args) {
RunnableDemo r = new RunnableDemo();
Thread t = new Thread(r);
//线程强制运行
//join强制
try {
t.start();
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
RunnableDemo r1 = new RunnableDemo();
Thread t1 = new Thread(r1);
t1.start();
RunnableDemo r2 = new RunnableDemo();
Thread t2 = new Thread(r2);
t2.start();
}
}
join
还有两个重载方法,可以去自己了解。
线程礼让
yield
是静态方法,直接用类调用就可以。
注意!!!
上面的设置优先级,是不能完完全全一个不漏的把控住的,只是优先级越高,先运行的机率越高。
yield的礼让也是如此,不是一定,是提高概率,不是绝对礼让。
而我们的
join
是绝对的
线程的同步
首先,我们需要了解,为什么同步。
为什么需要同步
线程的安全问题
- 多个线程执行的不确定性硬气执行结果的不稳定性
- 多个线程对账本的共享, 会造成操作的不完整性, 会破坏数据.
- 多个线程访问共享的数据时可能存在安全性问题
比如:
卖票过程中出现了重票和错票的情况 (以下多窗口售票demo存在多线程安全问题)。
当票数为1的时候,三个线程中有线程被阻塞没有执行票数-1的操作,这是其它线程就会通过if语句的判断,这样一来就会造成多卖了一张票,出现错票的情况。
极端情况为,当票数为1时,三个线程同时判断通过,进入阻塞,然后多执行两侧卖票操作。
所以,线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。
那么,线程同步的原因,就解释清楚了,我得准备一些代码来写关于同步锁的文章。
所以这篇文章先到这吧!