在理解进程和线程概念之前首选要对并发有一定的感性认识,如果服务器同一时间内只能服务于一个客户端,其他客户端都再那里傻等的话,可见其性能的低下,因此并发编程应运而生,并发是网络编程中必须考虑的问题。实现并发的方式有多种:比如多进程、多线程、IO多路复用。
多进程
进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。
Linux系统函数fork()
可以在父进程中创建一个子进程,这样的话,在一个进程接到来自客户端新的请求时就可以复制出一个子进程让其来处理,父进程只需负责监控请求的到来,然后创建子进程让其去处理,这样就能做到并发处理。
多线程
线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。
线程和进程各自有什么区别和优劣呢?
-
进程是资源分配的最小单位,线程是程序执行的最小单位。
-
因为进程拥有独立的堆栈空间和数据段,所以每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这对于多进程来说十分“奢侈”,系统开销比较大,而线程不一样,线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高。
-
但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。
-
进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便。
为什么要使用多线程:
多线程体现方式:
并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
图示如下:
好处:
1、使用多线程可以减少程序的响应时间(单线程指的是程序执行过程中只有一个有效操作序列,不同操作之间都有明确的先后顺序,如果某个操作很耗时,或者进入长时间的等待,此时程序将不会响应鼠标和键盘操作,使用多线程后,可以把这个耗时的操作分配到一个单独的线程中去执行,从而使线程具备更好的交互性)
2、与进程相比,线程的创建和切换开销更小。由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息。而运行于同一进程的线程共享代码段数据段,线程的启动和切换的开销比进程要少很多。同时多线程在数据共享方面效率非常高。
3、多CPU,多核计算机本身就具有执行多线程的能力。提高CPU利用率
4、使用多线程能简化程序的结构,使程序便于理解和维护。一个非常复杂的进程可以分为多个进程来执行。
多进程例子:
多线程下载:此时线程可以理解为下载的通道,一个线程就是一个文件的下载通道,多线程就是同时开启好几个下载通道,当服务器提供下载服务时,使用下载者是共享宽带的,在优先级相同的情况下,总服务器对总下载线程进行平均分配,不难理解你的线程多的话,下载越快。
Java多线程实现的方式有四种:
- 1.继承Thread类,重写run方法
- 2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
- 3.通过Callable和FutureTask创建线程
- 4.通过线程池创建线程
多线程安全问题解决:
原因:多个线程出现延迟
线程随机性
解决办法:
-
同步代码块
-
同步方法
-
Lock对象锁
代码实现:以卖票为例
没有实现同步时会出现余票为0,-1的情况
实现同步代码如下所示
1、同步代码块
package JavaTest;
public class SellDemo implements Runnable{
private int num = 50;
public void run() {
for (int i = 0; i < 100; i++) {
synchronized(this){ //同步代码块
if (num>0) {
try {
//不能直接调用getName()方法,所以要获得当前线程
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "出售了第:" + num + "张票");
num--;
}
}
}
}
public static void main(String[] args) {
SellDemo s = new SellDemo();
new Thread(s,"A").start();
new Thread(s,"B").start();
new Thread(s,"C").start();
}
}
2、同步方法
package JavaTest;
public class SellDemo implements Runnable{
private int num = 50;
public void run() {
for (int i = 0; i < 100; i++) {
gen();
}
}
public synchronized void gen(){
if (num>0) {
try {
//不能直接调用getName()方法,所以要获得当前线程
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "出售了第:" + num + "张票");
num--;
}
}
public static void main(String[] args) {
SellDemo s = new SellDemo();
new Thread(s,"A").start();
new Thread(s,"B").start();
new Thread(s,"C").start();
}
}
3、线程锁同步
package JavaTest;
import java.util.concurrent.locks.ReentrantLock;
public class SellDemo implements Runnable{
private int num = 50;
private final ReentrantLock lock = new ReentrantLock();
public void run() {
for (int i = 0; i < 100; i++) {
gen();
}
}
public void gen(){
lock.lock();
try {
if (num>0) {
try {
//不能直接调用getName()方法,所以要获得当前线程
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "出售了第:" + num + "张票");
num--;
}
}
finally{
lock.unlock();
}
}
public static void main(String[] args) {
SellDemo s = new SellDemo();
new Thread(s,"A").start();
new Thread(s,"B").start();
new Thread(s,"C").start();
}
}
线程状态转换
1、新建状态:就是新建了一个线程对象;
2、可运行状态:就是调用start()方法后,线程将进入一个线程池,等待系统分配资源,(注意不是说条用start()方法后,线程就被执行的,他得等待获得资源)
3、运行状态:就是系统给分配了资源(有的教材认为资源就是CPU的使用权),程序开始执行;
4、阻塞状态:由于某种原因,程序执行到某种程度时,放弃了资源的使用权,暂时停止运行。满足相应条件后,又变成可运行状态,这个由分为几种情况:
1)、等待阻塞:运行的线程执行了wait()方法,使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
2)、同步阻塞:对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)
3)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
5、结束:也叫死亡状态,就是程序执行完成了,或发生异常退出run()方法了。
此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。
各函数调用的作用和区别
1、run方法和start方法
系统调用线程类的start()方法来启动一个线程,此时该线程处于就绪状态,而非运行状态,也就意味着这个线程可以被JVM来调度执行。在调度过程中,JVM通过调用线程类的run()方法来完成实际操作,当run()方法结束后此线程就会终止。
如果直接调用线程类的run()方法,这会被当成一个普通的函数调用,程序中仍然只有主线程这一个线程,也就是说,start()能够异步调用run方法,但是直接调用run方法是同步的,无法达到多线程的目的。
2、sleep()方法和wait()方法
-
原理不同
sleep()是Thread类的静态方法,是线程用来控制自身流程的,它会使线程暂停一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程会自动苏醒。每隔一秒打印时间
wait()是Object类的方法,用于线程间通信,这个方法会使当前拥有该对象锁的进程等待,直到其它线程调用notify()(或者notifyAll()方法)时才会醒来。也可以指定时间自动醒来。
(sleep()指定的时间是线程不会运行的最短时间)
- 对锁的处理机制不同
sleep()不涉及到进程间通信,调用sleep()方法不会释放锁 举例:设置我拿电视遥控器期间用自己的sleep方法每个10秒调一下频道,在这10min中里遥控器还在我手中。
调用wait()方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他synchronized数据可以被别的线程使用。
- 使用区域不同
wait()必须放在同步语句块或者同步方法中
sleep()可以放在任何地方使用
sleep()必须捕获异常,而wait()、notify()、notifyAll()不需要
3、sleep()和yield()
- sleep()方法会给其他线程运行的机会,而不考虑其他线程的优先级,因此会给较低线程一个运行的机会;yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。
- 当线程执行了sleep(long millis)方法后,将转到阻塞状态,参数millis指定睡眠时间,在指定时间内线程不会运行;当线程执行了yield()方法后,将转到就绪状态,可能马上又进入可执行状态执行。
- sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明抛出任何异常
- sleep()方法比yield()方法具有更好的移植性
4、wait、notify()和notifyAll()
Object 是所有类的超类,它有 5 个方法组成了等待/通知机制的核心:notify()、notifyAll()、wait()、wait(long)和 wait(long,int)。在 Java 中,所有的类都从 Object 继承而来,会抛出java.lang.IllegalMonitorStateException。因此,所有的类都拥有这些共有方法可供使用。而且,由于他们都被声明为 final,因此在子类中不能覆写任何一个方法。
wait()
在同步方法或同步块中调用 wait()方法。进入 wait()方法后,当前线程释放锁。在从 wait()返回前,线程与其他线程竞争重新获得锁。如果调用 wait()时,没有持有适当的锁,则抛出 IllegalMonitorStateException,它是 RuntimeException 的一个子类,因此,不需要 try-catch 结构。
notify()
该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,的如果调用 notify()时没有持有适当的锁,也会抛出 IllegalMonitorStateException。该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个 wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify 后,当前线程不会马上释放该对象锁,wait 所在的线程并不能马上获取该对象锁,要等到程序退出 synchronized 代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的 wait 线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他 wait 状态等待的线程由于没有得到该对象的通知,会继续阻塞在 wait 状态,直到这个对象发出一个 notify 或 notifyAll。这里需要注意:它们等待的是被 notify 或 notifyAll,而不是锁。这与下面的 notifyAll()方法执行后的情况不同。
notifyAll()
该方法与 notify ()方法的工作方式相同,重要的一点差异是:
notifyAll 使所有原来在该对象上 wait 的线程统统退出 wait 的状态(即全部被唤醒,不再等待 notify 或 notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll 线程退出调用了 notifyAll 的 synchronized 代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
深入理解
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
6、join()方法
参考博文https://www.cnblogs.com/lcplcpjava/p/6896904.html
Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。
public class JoinTest {
public static void main(String [] args) throws InterruptedException {
ThreadJoinTest t1 = new ThreadJoinTest("小明");
ThreadJoinTest t2 = new ThreadJoinTest("小东");
t1.start();
/**join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕
所以结果是t1线程执行完后,才到主线程执行,相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会
*/
t1.join();
t2.start();
}
}
class ThreadJoinTest extends Thread{
public ThreadJoinTest(String name){
super(name);
}
@Override
public void run(){
for(int i=0;i<1000;i++){
System.out.println(this.getName() + ":" + i);
}
}
}
上面程序结果是先打印完小明线程,在打印小东线程;
上面注释也大概说明了join方法的作用:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。注意,这里调用的join方法是没有传参的,join方法其实也可以传递一个参数给它的,具体看下面的简单例子:
public class JoinTest {
public static void main(String [] args) throws InterruptedException {
ThreadJoinTest t1 = new ThreadJoinTest("小明");
ThreadJoinTest t2 = new ThreadJoinTest("小东");
t1.start();
/**join方法可以传递参数,join(10)表示main线程会等待t1线程10毫秒,10毫秒过去后,
* main线程和t1线程之间执行顺序由串行执行变为普通的并行执行
*/
t1.join(10);
t2.start();
}
}
class ThreadJoinTest extends Thread{
public ThreadJoinTest(String name){
super(name);
}
@Override
public void run(){
for(int i=0;i<1000;i++){
System.out.println(this.getName() + ":" + i);
}
}
}
上面代码结果是:程序执行前面10毫秒内打印的都是小明线程,10毫秒后,小明和小东程序交替打印。
所以,join方法中如果传入参数,则表示这样的意思:如果A线程中掉用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。
join方法必须在线程start方法调用之后调用才有意义。这个也很容易理解:如果一个线程都没有start,那它也就无法同步了。
join方法的原理就是调用相应线程的wait方法进行等待操作的,例如A线程中调用了B线程的join方法,则相当于在A线程中调用了B线程的wait方法,当B线程执行完(或者到达等待时间),B线程会自动调用自身的notifyAll方法唤醒A线程,从而达到同步的目的。
线程结束方法
- 使用stop强行终止线程
使用stop方法可以强行终止正在运行或挂起的线程。我们可以使用如下的代码来终止线程:
thread.stop(); 虽然使用上面的代码可以终止线程,但使用stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,因此,并不推荐使用stop方法来终止线程。
- 使用退出标志终止线程
一般是将这些任务放在一个循环中,如while循环。如果想让循环永远运行下去,可以使用while(true){……}来处理。但要想使while循环在某一特定条件下退出,最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。下面给出了一个利用退出标志终止线程的例子。
public class ThreadFlag extends Thread
{
public volatile boolean exit = false;
public void run()
{
while (!exit);
}
public static void main(String[] args) throws Exception
{
ThreadFlag thread = new ThreadFlag();
thread.start();
sleep(5000); // 主线程延迟5秒
thread.exit = true; // 终止线程thread
thread.join();
System.out.println("线程退出!");
}
}
上面代码中定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值,
- 使用interrupt终止线程
使用interrupt方法来终端线程可分为两种情况:
(1)线程处于阻塞状态,如使用了sleep方法。
(2)使用while(!isInterrupted()){……}来判断线程是否被中断。
在第一种情况下使用interrupt方法,sleep方法将抛出一个InterruptedException例外,而在第二种情况下线程将直接退出。
下面的代码演示了在第一种情况下使用interrupt方法。
public class ThreadInterrupt extends Thread
{
public void run()
{
try
{
sleep(50000); // 延迟50秒
}
catch (InterruptedException e)
{
System.out.println(e.getMessage());
}
}
public static void main(String[] args) throws Exception
{
Thread thread = new ThreadInterrupt();
thread.start();
System.out.println("在50秒之内按任意键中断线程!");
System.in.read();
thread.interrupt();
thread.join();
System.out.println("线程已经退出!");
}
}
上面代码的运行结果如下:
在50秒之内按任意键中断线程!
sleep interrupted
线程已经退出!
在调用interrupt方法后, sleep方法抛出异常,然后输出错误信息:sleep interrupted.
注意:在Thread类中有两个方法可以判断线程是否通过interrupt方法被终止。一个是静态的方法interrupted(),一个是非静态的方法isInterrupted(),这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted可以用来判断其他线程是否被中断。因此,while (!isInterrupted())也可以换成while (!Thread.interrupted())。
守护线程
Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)
用户线程即运行在前台的线程,而守护线程是运行在后台的线程。 守护线程作用是为其他前台线程的运行提供便利服务,而且仅在普通、非守护线程仍然运行时才需要,比如垃圾回收线程就是一个守护线程。当VM检测仅剩一个守护线程,而用户线程都已经退出运行时,VM就会退出,因为没有如果没有了被守护者,也就没有继续运行程序的必要了。如果有非守护线程仍然存活,VM就不会退出。
守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。用户可以用Thread的setDaemon(true)方法设置当前线程为守护线程。
虽然守护线程可能非常有用,但必须小心确保其他所有非守护线程消亡时,不会由于它的终止而产生任何危害。因为你不可能知道在所有的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦所有的用户线程退出了,虚拟机也就退出运行了。 因此,不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。
另外有几点需要注意:
1、setDaemon(true)必须在调用线程的start()方法之前设置,否则会抛出IllegalThreadStateException异常。
2、在守护线程中产生的新线程也是守护线程。
3、 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。