多线程
什么是线程
线程就像进程的“分身”,进程在同一时间只能干一件事,如果需要同时干多件事,就需要线程,一个线程就是一个“执行流”,多个线程之间可以“同时”执行。
引入线程的原因
随着发展,单核CPU的发展接近极致,为了继续提高算力,就引入了多核CPU,而所谓的并发编程就是为了最大程度上充分地利用多核资源;虽然多进程也可以实现并发编程,但是如果频繁的创建进程再销毁进程,就会较为低效,而多线程就可以更好地解决这个问题,因此线程也被称为“轻量级编程”。
为什么线程较进程要更轻量呢?其实是由于进程和线程创建过程的区别:
进程的创建过程简单可以理解为:
①创建PCB;
②分配系统资源,比较消耗时间;
③把PCB加入到内核的双向链表中;
线程的创建过程:
①创建PCB;
②把PCB加入到内核的双向链表中;
线程的创建过程是对进程的一个改进,没有了较为消耗时间的分配资源操作,也就更加轻量。
多线程的一些问题
多线程的引入是为了提高效率,但是如果多核资源紧张,有可能就会适得其反,因此多核资源充分是多线程提高效率的主要前提;
多线程编程的难点是线程安全问题,当多个线程访问同一个变量,就可能引发线程安全问题;
当一个进程中的一个线程出现了问题,比如抛出了异常,必须要及时进行处理,否则就可能会导致整个进程的崩溃;
进程和线程的区别(!)
- 进程包含了线程,一个进程可以有多个线程,最少有一个线程,即主线程;
- 进程之间不会共享同一块内存(隔离性),一个进程的不同线程之间共享同一块内存;
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位;
- 当多个进程同时执行,一个进程出现故障,一般不会影响其它进程(进程的独立性),一个进程中的多个线程同时执行,一个线程出现故障,会影响到其他线程;
创建线程
一个多线程程序
class MyThread extends Thread{
@Override
public void run() {
System.out.println("hello thread");
}
}
public class Test1 {
public static void main(String[] args) {
Thread t=new MyThread(); //向上转型
t.start();
System.out.println("hello main");
}
}
一个进程至少有一个线程,当运行上面的程序,操作系统就会调用一个java进程,在这个java进程中的一个线程调用了main( )方法,这个线程就是主线程;
一般我们认为,对于进程而言,如果在进程A中创建了进程B,我们就称进程A为进程B的父进程,进程B为进程A的子进程;但是对于线程而言,一般认为线程之间就是并行的,不存在父线程或子线程的说法。
上面的运行结果显示是首先打印了 hello main,但实际上每个线程都是独立的执行流,这些执行流之间的执行是并发进行的,两个线程之间的执行顺序取决于操作系统的内部调度,于我们而言,就是随机的。因此,即使我们运行数次程序得到的打印结果都是一致的,我们也不能轻易确定下一次程序的执行结果;
很多时候,虽然我们没有手动创建许多线程,但当程序运行调用java进程,java进程中一般就会包含许多线程,那么如何查看有哪些线程正在执行呢?
为了可以清楚的看到正在执行的线程,我们使用死循环的方式延长程序的执行时间:
class MyThread extends Thread{
@Override
public void run() {
while (true){
System.out.println("hello thread");
}
}
}
public class Test1 {
public static void main(String[] args) {
Thread t=new MyThread();
t.start();
while (true){
System.out.println("hello main");
}
}
}
使用 jconsole.exe查看正在执行的进程:
双击运行:
找到线程:
这里的main与Thread-0就是我们刚刚程序运行中自己的线程,而其他的线程就是java进程中的:
利用死循环的方式,程序执行过快,就可以使用Thread.sleep( )方法来进行改进:
class MyThread extends Thread{
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test1 {
public static void main(String[] args) {
Thread t=new MyThread();
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
通过运行结果,是不是也更加清楚的观察到2个线程的调度是随机的呢!
创建多线程程序
- 创建一个类继承Thread,重写其中的run( )方法;
上面多线程程序的创建其实就是采用了这种方法,在run( )内部指明这个多线程是要完成什么任务,然后创建实例就是将任务交给这个新创建的引用,最后使用start( )方法才是真正创建了线程;
- 创建一个类实现Runnable,重写run( )方法;
class MyRunnable implements Runnable{
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test2 {
public static void main(String[] args) {
Runnable runnable=new MyRunnable(); //创建实例
Thread t=new Thread(runnable); //将任务交给线程
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
前面创建类继承类的方式,实际是将线程与任务内容绑定在了一起;而这种实现接口的方法则是做到了线程和任务的分离;
- 创建一个类继承Thread类,非显示继承,使用匿名内部类;
public class Test3 {
public static void main(String[] args) {
//匿名内部类
Thread t=new Thread(){
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
}
}
- 以匿名内部类的方式使用Runnable;
public class Test4 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
}
}
- 使用lambda表达式来定义任务;
public class Test5 {
public static void main(String[] args) {
Thread t=new Thread(()->{
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
lambda表达式的本质是一个匿名函数,由参数和返回值两部分组成,语法格式是:(参数)- > 返回值
(参数)- > {返回值} (参数可以有多个,也可以没有);
多线程编程的优势
多线程编程的最大优势就是可以提高运行速度,下面通过单线程和多线程对于运行时间的长短来证明这一点:
首先是一个单线程的程序:
public class Test6 {
private static final long count=50_0000_0000l;
private static void serial(){
//使用System.currentTimeMillis()来记录当前的毫秒时间戳
long beg=System.currentTimeMillis();
int a=0;
for(long i=0;i<count;i++){
a++;
}
a=0;
for(long i=0;i<count;i++){
a++;
}
long end=System.currentTimeMillis();
System.out.println("单线程消耗的时间: " +(end-beg)+" ms ");
}
private static void concurrency(){
long beg=System.currentTimeMillis();
Thread t1=new Thread(()->{
int a=0;
for(long i=0;i<count;i++){
a++;
}
});
Thread t2=new Thread(()->{
int a=0;
for(long i=0;i<count;i++){
a++;
}
});
t1.start();
t2.start();
try {
t1.join();//调用join方法为了使t1的线程执行结束主线程再执行
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end =System.currentTimeMillis();
System.out.println("并发执行消耗的时间:"+(end-beg)+ " ma");
}
public static void main(String[] args) {
serial();
concurrency();
}
}
运行结果:
通过运行结果可以看出,并发执行的方式执行速度要更快;
join()方法要做的事就是,当有新的线程加入时,主线程会进入等待状态,一直到调用join()方法的线程执行结束为止,就有效避免了主线程对执行时间的影响;
多线程的使用场景
- CPU密集场景
前面提到,多线程的引入就是为了最大程度的利用CPU的多核资源,因此,在CPU密集场景使用多线程编程也就再合适不过了; - IO密集型场景
IO即输入输出,因为在IO密集型场景中,几乎不需要CPU就能快速完成读写数据的操作,一般都需要很大的时间等待,这时,使用多线程就可以避免CPU过于闲置。
over!