从零开始学Java多线程以及线程优化(一)

基础概念

一、CPU

      理论上CPU的核心数与线程数是1:1的,比如4核的CPU,只能同时执行4个任务,但Intel推出了超线程技术后,它把一个物理的CPU通过超线程技术模拟成了2个逻辑CPU,这样核心数与线程数就了1:2,可同时执行的任务数提高了一倍,这就是四核八线程的由来。

     现在又有一个问题,既然4核的CPU最多也才能同时执行8个线程,可为什么我们平时开发中new Thread张口就来,也没见什么限制呢?这就牵涉到另一个技术--CPU时间片轮转机制(RR调度),比如现在有一个CPU,马上有100个任务要执行,RR调度把这100个任务按先来后到的顺序排成队,每一个任务分配大约10ms的时间在CPU上执行,时间到后马上这个任务跟CPU断开,再让这个任务返回队伍重新排队,让排第二位的任务继续进CPU运行,以此往复。整个队列执行一遍也才花了1s,人类的感知看来好像是同时执行的。

二、进程和线程

      进程:操作系统在运行时资源分配的最小单位,它是线程的容器,是程序的实体。对于Android来说,启动了一个App就是开启了一个进程,进程启动后操作系统为它分配CPU、内存空间、磁盘IO等等。

      线程:CPU能做调度运算的最小单位,它被包含于进程之中,一个进程可以开启多个线程,执行不同的任务。一个进程内地所有线程共享进程内的全部系统资源。

      线程依附于进程存在,如果进程内至少还有一个线程存在,进程就还在,如果所有的线程都被不在了,进程也会被杀死。

      启动一个进程后默认有一个线程在运行。Android来说就是MainThread,也叫UIThread。UIThread主要任务就是绘制UI界面,如果进行太多耗造成线程阻塞超过5S就会造成ANR-应用程序无响应。

      进程和CPU之间没有必然关系,上文所说RR调度期间同时执行的8个线程,至于是哪几个线程,哪个进程的线程,这是由进程的活跃度决定的,所以会同一时间有可能一个进程的所有线程都不在CPU上也是存在的。

      操作系统可支持的存在的最大线程数:Linux 1000 ;Windows 2000。

      Java不能指定由哪个CPU运行指定的线程,C语言可以。

三、并行和并发

      并行:有同一时间处理多个任务的能力,关键是你有同时处理多个任务的能力。

      并发:单位时间内执行任务的数量,比如CPU在1s内轮转了100个时间片,我们就说它的并发能力是100。

四、高并发编程的意义及好处

      现在的CPU没有不支持多线程并发机制的,所以在编程时使用多线程充分的利用CPU,发挥它的运算能力,提高并发量,能成倍提高程序运行的速度。再有一个好处就是可以使代码模块化,异步化.、简单化。

     多线程程序需要注意事项:

    1、统一进程内的所有线程是共享资源的,所以不同的线程都可以访问同一个内存地址当中的一个变量。当不同的线程同时访问同一个全局变量或静态变量时,如果只有读操作,则线程安全不会出问题,但同时执行写操作,并且没有线程同步,则会出现线程安全问题。

    2、为了解决线程间的安全问题,Java引入了锁机制,但当出现死锁后,所有的线程都在等待不可能被释放的锁,从而导致所有工作都无法完成,最终出现线程死循环。https://blog.csdn.net/lyabc123456/article/details/81060477

   3、线程太多把服务器资源耗尽:这里的服务器资源包括内存、访问文件的文件描述符等。App每开一个线程,Java虚拟机最少都要开辟相应的1M的栈空间,一个App内存空间在一定时间内是固定的,当开启了大量线程超过App内存空间后,就会造成OOM。

     "过渡切换"的问题。前文说的”CPU时间片轮转机制“,当一个线程运行完一个时间片后,就要从CPU里出来并把线程相关的资源保存到内存或磁盘,下一个线程进入CPU前当然要从内存或磁盘取出资源,然后才开始跑属于它的时间片,这一存一取就叫”上下文切换“。一个上下文切换所耗费的时间大概为20000个CPU时间单位,运行一次1+1运算也才差不多1个时间单位。所以频繁的切换也会大量的消耗CPU资源。这种情况在开启大量线程的情况下尤为明显, 比如一个程序运行开启了100个线程,相同任务下这100线程所用的时间还没有只开一个线程快。这种情况在实际开发中更多是用线程池处理,这个以后再说。

Java的多线程

   java天生是多线程的,最简单的运行一个main()方法,启动就启动了6个线程:

    public static void main(String[] args) {
    	ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
    	ThreadInfo[] threadInfos = 
    	 threadMXBean.dumpAllThreads(false, false);
    	for(ThreadInfo threadInfo:threadInfos) {
    		System.out.println("["+threadInfo.getThreadId()+"]"+" "
    				+threadInfo.getThreadName());
    	}
    }

打印结果

开启线程的方法

1、扩展Thread类,复写run()方法;

		new Thread(){
			@Override
			public void run() {
				super.run();
				System.out.println("我直接new了一个,复写run()方法");
			}
		}.start();

2、实现Runnable接口,把实例交给Thread执行。

public class RunnableImp implements Runnable{
    @Override
    public void run() {
        System.out.println("实现Runnable后复写run()方法");
    }
}


RunnableImp runnableImp=new RunnableImp();
new Thread(runnableImp).start();

3、实现Callable,结合FutureTask<T> 包装任务后交给Thread执行。

public class CallableImp implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("实现Callable<String>后复写 call()方法");
        return "我可以返回一串数据";
    }
}

Callable相比Runnable多了返回值,意味着我们线程执行完后可以直接得到结果。但Callable不能直接交给Thread执行,我们看Thread的构造方法

所以要结合FutureTask包装Callable,我们看下FutureTask类

FutureTask实现了RunnableTask,RunnableTask又继承了Runnable和Future接口。Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。所以FutureTask既可以被当成一个Runnable被Thread执行,又可以包装了Callable后得到返回值。

CallableImp callableImp=new CallableImp();
FutureTask<String> futureTask=new FutureTask<>(callableImp);
new Thread(futureTask).start();
System.out.println("线程执行完返回的结果:"+ futureTask.get());

Thread是Java里对线程的抽象,Runnable和Callable是对任务的抽象,能启动线程的只有Thread。

Thread中止

Thread的暂停、恢复、停止对应的方法为suspend()、resume()和stop(),但这些方法都已被官方加了@Deprecated注解,不推荐使用,比如stop()方法在停止线程时,比如线程执行任务到到三分之二时调用了stop()方法,这是线程资源还没释放就被停止了,程序可能因此工作在不确定的状态下。suspend()方法在调用后线程不会释放所占有的资源,这是该线程会连通占有的资源进入睡眠,造成上文说的死锁状态。

interrupt()、isInterrupted()、interrupted()

安全的中止线程的方法是interrupt(),因为Java的线程是协作性的,interrupt()方法不会强制性的停止线程,它只是给线程置一个中断标识位,表示线程该停下来了,如果我们不进行其他的操作,线程仍然不会搭理它,一直到执行完任务才会停止。

所以我们要安全的停止线程还要配合使用isInterrupted(),这个方法在我们调用后会返回true,这时我们进行自己的停止操作。

    public static class InterruptThread extends Thread{
        @Override
        public void run() {
            super.run();
            System.out.println("---当前Interrupt flag:"+isInterrupted()+"---");
            while (!isInterrupted()){
                System.out.println("while 循环内Interrupt flag:"+isInterrupted());
            }
            System.out.println("---while 循环结束后Interrupt flag:"+isInterrupted()+"---");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        InterruptThread interruptThread=new InterruptThread();
        interruptThread.start();
        Thread.sleep(1);
        interruptThread.interrupt();
    }

打印结果

----------------------------当前Interrupt flag:false----------------------------
while 循环内Interrupt flag:false
while 循环内Interrupt flag:false
while 循环内Interrupt flag:false
while 循环内Interrupt flag:false
while 循环内Interrupt flag:false
.
.
.
while 循环内Interrupt flag:false
while 循环内Interrupt flag:false
while 循环内Interrupt flag:false
while 循环内Interrupt flag:false
----------------------------while 循环结束后Interrupt flag:true----------------------------

还有一个静态方法interrupted(),这个方法也可以返回中断标识位的状态,有意思的是当我们调用interrupt()方法把中断标识位置true,interrupted()返回一次true后立马又变成了false

    public static class InterruptThread extends Thread{
        @Override
        public void run() {
            super.run();
            System.out.println("---当前Interrupt flag:"+interrupted()+"---");
            while (!interrupted()){//这里使用了静态的interrupted()判断标识位
                System.out.println("while 循环内Interrupt flag:"+interrupted());
            }
            System.out.println("---while 循环结束后Interrupt flag:"+interrupted()+"---");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        InterruptThread interruptThread=new InterruptThread();
        interruptThread.start();
        Thread.sleep(1);
        interruptThread.interrupt();
    }

打印结果

----------------------------当前Interrupt flag:false----------------------------
while 循环内Interrupt flag:false
while 循环内Interrupt flag:false
while 循环内Interrupt flag:false
.
.
.
while 循环内Interrupt flag:false
while 循环内Interrupt flag:false
while 循环内Interrupt flag:false
----------------------------while 循环结束后Interrupt flag:false---------------------------

根据打印的结果来判断,interrupted()最后肯定返回了一次true,但马上中断标识位就又变回了false。

所有isInterrupted()和静态的interrupted()方法的区别是,isInterrupted()方法把中断标识位置为true后就一直是true,interrupted()把中断标识位置为true后,中断标示位又会马上变成false,至于这样有什么用,再研究。

start()和run()方法

我们new Thread()只是在JDK里创建了一个Thread的实例,只有在调用了start()方法后又调用了native方法start0(),经过C语言的一顿处理后,才在操作系统里生成了一个线程 ,这是处于就绪状态,经过CPU的调度排序,轮到此线程执行它的时间片时才会执行run()方法,当时间片执行完毕任务还没有完成就又CPU至于就绪状态,继续接受CPU的调度,等到某一个时间片任务执行完毕或调用了stop()方法,线程才会结束。

当我们在线程运行的时候调用了sleep()或wait()时,线程会进入阻塞状态,不同的是调用sleep()会在sleep时间到后重新进入就绪状态,调用wait()后只有手动调用notify()或notifyAll()才会重新进入就绪状态,继续接受CPU的调度。

同一Thread实例只能调用一次start(),重复调用会出错。

run()方法只是一个普通的成员方法,new Thread().run()是在主线程执行的。new Thread().start();run()方法才会在子线程执行。

 

从零开始学Java多线程以及线程优化(二)

https://blog.csdn.net/ssrmyxs/article/details/105747220

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值