Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
1.多线程带来的问题?
很难编写一个线程安全的程序
线程安全从现象上来描述是在多线程环境中运行结果和单线程运行结果一致。那怎么做到线程安全呢?第一选择避免共享资源,如果避免不了使用共享资源,那么就要保证共享资源的原子性和一致性。
但是在编写代码的时候,是很难做到在多线程中不使用共享资源,而多线程见对共享资源的控制难度比较大。其实Java提供了了最简单的解决并发的方式,性能还不错,就是synchronized,但是这个太简单了,带来的问题就是无法灵活地操作这把锁,比如设置锁的超时时间,立即释放锁,无法通信。有了Lock,Java并发的世界打开了一个新的视角,要编写一个正确的线程安全的程序,对于高手来说也是一项挑战。
由于线程执行顺序的不可预知和随机性,多线程的程序难以调试,难以重现。“你的提这个BUG,我重现不了”,这句话是不是很熟悉呢?Java采用抢占式方式对线程进行调度,在这种调度下,有些线程由于CPU分配的时间片较多会执行的时间很长,而有些线程抢占不到CPU的时间片得不到执行。
除了抢占式调度还有协同式调度,协同式调度是线程执行完后主动通知系统切换到其他线程上,从实现上来说协同式调度很简单,不需要线程同步。缺点是一个线程有问题,就会阻塞到那里,因此Java采用了抢占式调度。
线程间对共享资源的抢夺,更严重的会造成死锁。比如: A线程已经占用资源1的锁,要抢占资源2的锁,而B线程已经占用资源2的锁,要抢占资源1的锁,线程AB互不释放持有的锁,一直等待,就会发生死锁。发生死锁需要满足四个条件,破坏其中任何一个就可以避免死锁,不过Java没有死锁处理机制,不会自动避免死锁。
2 线程的属性、状态、生命周期详解
Java线程具有五中基本状态
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
3.ThreadLocal是什么?
ThreadLocal是一个关于创建线程局部变量的类。
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。
4.为什么要使用线程池
在有些工作场景中,比如说服务器编程中,如果为每一个客户都分配一个新的工作线程,并且当工作线程与客户通信结束时,这个线程被销毁,这就需要频繁的切换工作线程,这会带来一些负担,最主要的是系统大的开销和系统资源不足问题。
首先,服务器创建和销毁工作线程的开销很大,如果服务器与很多客户端通信,并且与每个客户端通信的时间很短,那么就会在创建和销毁线程的时候造成很大的开销。
其次,活动的线程也消耗系统资源,如果线程的创建数量没有限制,当大量的客户连接服务器的时候,就会创建出大量的工作线程,他们会消耗大量的内存空间,导致系统的内存空间不足,影响服务器的使用。
最后,如果线程的数目固定,并且每个线程都有很长的生命周期,那么线程切换也就是固定的,这样就会给服务器减轻很多压力,但是如果频繁的创建和销毁线程,必将导致频繁的切换线程,使得线程之间的切换不再遵循系统的固定切换周期,线程切换的开销也会增大很多。
线程池为这些问题提供了解决方案。线程池中预先创建了一些工作线程,他们不断的从工作队列中取出任务,然后执行,当执行完之后,会继续执行工作队列中的下一个任务,减少了创建和销毁线程的次数,每个线程都可以一直被重用,避免了创建和销毁的开销。另外,可以根据系统的实际承载能力,方便的调节线程池中线程的数目,防止因为消耗过量的系统资源而导致系统崩溃的问题。