多线程
线程与进程
概念
进程(process):是正在计算机中执行的程序。
线程(thread): 是进程中的某个单一的顺序的控制流。
注:
进程是应用程序的运行实例,自己享用独立的地址空间。线程是进程的细化,他是进程的实体。
进程是系统进行资源分配和调度的最小单位。线程是CPU调度和分派的最小单位。
区别
从一个进程转换到另一个进程 | 通信 |
---|---|
进程 | 开销很大 |
线程 | 开销小 |
原因:进程有独立的地址空间;线程共享相同的地址空间并且共同分享同一进程
多线程的利与弊
利:解决多部分同时运行的问题
弊:线程太多会导致效率低
注:实质是CPU进行程序的快速切换,且是随机的
JVM中的多线程
jvm启动时就启动多线程,且至少有两个
①执行main函数的线程
该线程的任务代码都定义在main函数中。
②负责垃圾回收的线程
注:①System.gc();//呼叫java虚拟机的垃圾回收器运行
②finalize //类似 c++中析构函数,表示对象即将消亡时,调用此方法
class Demo1 extends Object
{
@Override
public void finalize()
{
System.out.println("垃圾回收");
}
}
创建多线程
继承Thread类
步骤:
①定义一个类并继承Thread类
②覆盖Thread类中的run();方法
③创建一个Thread子类的对象创建多线程
④调用start();方法开启多线程并调用run();方法
/**
* 继承Thread创建多线程
*/
public class MyThread extends Thread {
private String threadName = null;
public MyThread(String threadName) {
this.threadName = threadName;
}
@Override
public void run() {
int i = 0;
while (i < 100) {
System.out.println("线程" + threadName + "正在运行");
i++;
}
}
}
Thread thread = new MyThread( " 继承Thread " );
thread.start ();
实现Runnable接口
步骤:
①定义类实现Runnable接口
②覆盖run()方法
③通过Thread类创建线程对象,并将Runnable接口子类对象作为Thread类的构造函数的参数进行传递
④调用线程对象start()方法开启线程
/**
* 实现Runnable创建多线程(常用)
*/
public class MyRunnable implements Runnable {
private String threadName = null;
public MyRunnable(String threadName) {
this.threadName = threadName;
}
@Override
public void run() {
int i = 0;
while (i < 100) {
System.out.println("线程" + threadName + "正在运行");
i++;
}
}
}
Runnable runnable = new MyRunnable( " 实现Runnable " );
Thread thread1 = new Thread (runnable );
thread1.start ();
注:①获取线程的名称,getName();
②因为创建线程时,已对线程进行编号,故单单getName()无法获取当前线程的名字。需要用Thread.currentThread.getName();获取当前的线程名字。可以通过继承Thread类,调用父类构造方法,设置线程的名称。 super(threadName);
③主函数的线程名字为:main
④线程栈:一个线程会有自己独立的栈空间,且throw抛出异常时不会相互影响
/**
* 【源码】
* Thread与Runnable 区别代码解释
* 利用构造函数的参数进行区分
*/
class Thread
{
private Runnable tager;
public Thread(){ }
public Thread(Runnbaler)
{ this.tager = r; }
public void run()
{ if(tager != null)
{ tager.run(); }
}
}
run()与start()
两种方法的区别:
1.start方法
用 start方法来启动线程,是真正实现了多线程, 通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法。但要注意的是,此时无需等待run()方法执行完毕,即可继续执行下面的代码。所以run()方法并没有实现多线程。
2.run方法
run()方法只是类的一个普通方法而已,如果直接调用run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码。
线程的五种状态
①新状态:线程已经被创建但尚未执行(未执行start()方法)
②可执行状态:线程可执行,但是不一定正在执行。CPU时间随时可能被分配给该线程,从而使得它执行。
③死亡状态:正常情况下,run()方法返回使得线程死亡。线程一旦死亡,就不能复生。
④阻塞状态:具备执行资格,但是不具备执行权,正在等待执行权。
⑤运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
注:
1.一个线程不能多次启动
2.CPU的执行资格:可以被CPU处理
CPU的执行权:正在被CPU处理
多线程产生安全问题
原因:多线程操作共享数据,且执行代码有两条及以上时,其他线程参与运算会导致线程出现安全问题
解决思路:将需要多线程的代码封装在一起,防止当一个线程运行时有其他线程打断该线程的运行。
1.同步函数
public synchronized void show() {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ".....function...." + num--);
}
}
2.同步代码块
Object a = new Object();
public void running() {
synchronized (a) {
if (ticket > 0) {
System.out.println(
"售票员" + Thread.currentThread().getName() + "出售车票,还剩" + --ticket + "车票");
}
}
}
3.静态同步函数
/**
* 静态同步代码块
*/
public static synchronized void show() {
if (ticket > 0) {
System.out.println(
"售票员" + Thread.currentThread().getName() + "出售车票,还剩" + --ticket + "车票");
}
}
注:
1.(任意)对象相当于一个开关,控制线程的单独运行
利:解决了线程安全问题
弊:效率相对较低(要判断同步锁)
同步锁的前提:必须使用多线程并共用一把锁,即共用一个Object b = new Object();
2.三者之间主要的”锁“的不同
(用synchronized修饰函数)使用的锁时this
(用synchronized修饰的代码块)同步代码块的锁是任意的(常用)
(用static synchronized修饰)
使用该函数所属的字节码文件对象为锁;
可通过 getClass();或者通过当前类名.class 表示
多线程的通信
①等待/唤醒机制
wait() :让线程处于冻结状态,被wait线程存储在线程池中
notify() :唤醒线程池中的一个线程(任意)
notifyAll():唤醒线程池中的所有线程
注:这些方法都必须定义在同步中,因为要明确是哪个锁上的线程,用于操作线程状态;
为什么操作线程的方法wait notify notifyAll定义在了Object类中?
因为这些方法是监视器的方法。监视器其实就是锁。锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中。
wait()与sleep()的区别?
1.wait 可以设置时间
sleep 必须设置时间
2.wait 释放执行权,释放锁
sleep 释放执行权,不释放锁(占着CPU睡觉)
注:两个都必须try
package name.liushiyao.thread.deadlock;
/**
* Created by 电子小孩 on 2017/3/17.
*/
/**
* 线程死锁问题:由于多个线程访问共享数据时,由于访问的顺序不当导致的死锁问题
*
* 解决办法:避免多线程访问共享数据(使用同一个锁),或者改正访问顺序
*
*
* 背景:两个人使用同一双筷子吃饭。两个筷子必须同时被同一个人使用才可以吃饭。
*
* 发现问题:如果把两把锁没有设置为static时,不存在”死锁问题” 猜想:是不是由于没有用static修饰,所以两个锁都不是共享数据,所以不存在多线程操作共享数据导致的死锁问题
* 探究:判断object1 object2的引用地址是否为相同?
*
* object11的引用地址为:java.lang.Object@22b028d6 object21的引用地址为:java.lang.Object@6c4484a0
* object12的引用地址为:java.lang.Object@62836758 object21的引用地址为:java.lang.Object@2c77c2f0
*
* 可以看出,object11和object12不是同一个对象,所以不是同一把锁,不存在死锁问题
*/
public class DeadLockThread implements Runnable {
//创建两把object锁
private static Object object1 = new Object();
private static Object object2 = new Object();
//筷子先被A或B拿起的标志
public boolean flag;
@Override
public void run() {
if (flag) {
synchronized (object1) {
// System.out.println ("object11的引用地址为:"+object1 ); //检验static
System.out.println("A拿起了一支object1筷子,等待拿起另外一只筷子object2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2) {
// System.out.println ("object21的引用地址为:"+object2 ); //检验static
System.out.println("A又拿起了一支object2筷子");
}
}
} else {
synchronized (object2) {
// System.out.println ("object21的引用地址为:"+object2 ); //检验static
System.out.println("B拿起了一支object2筷子,等待拿起另外一只筷子object1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object1) {
// System.out.println ("object12的引用地址为:"+object1 ); //检验static
System.out.println("B又拿起了一支object1筷子");
}
}
}
}
}
停止线程
run方法中都会出现循环结构,控制循环即可停止线程。
(1)标记法
定义boolean类型的flag作为线程结束的标记
public class StopThreadDemo {
public static void main(String[] args) {
StopThreads = new StopThread();
Thread a = new Thread(s);
Thread b = new Thread(s);
a.start();
b.start();
int num = 0;
for (; ; ) {
if (++num == 50) {
s.stopThread();
break;
}
System.out.println("main....." + num);
}
System.out.println("over-------");
}
}
class StopThread implements Runnable {
private Boolean flag = true;
public void stopThread() {
this.flag = false;
}
public void run() {
while (flag) {
System.out.println(Thread.currentThread().
getName() + "线程运行");
}
}
}
(2)Interrupt
当线程处于冻结状态,无法读取标记,则可以通过interrupt()方法将线程从冻结状态强制恢复到运行状态中,让线程具备cpu执行资格。
public synchronized void run() {
while (flag) {
try {
wait(); //处于冻结状态
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + ".........." + e);
flag = false;
}
System.out.println(Thread.currentThread().getName() + "线程运行");
}
}
if(++num ==50)
{
// s.stopThread();
a.interrupt();
b.interrupt();
break;
}
}
注:但是强制动作会发生InterruptedException异常,记得处理
使用Thread.suspend()被打断不会导致InterruptedException异常;
(3)异常
抛出异常也会是线程停止。
守护线程
概念
Java有两种Thread:“守护线程Daemon”(守护线程)与“用户线程User”(非守护线程)。
用户线程:非守护线程包括常规的用户线程或诸如用于处理GUI事件的事件调度线程,Java虚拟机在它所有非守护线程已经离开后自动离开。
守护线程:守护线程则是用来服务用户线程的,比如说GC线程。如果没有其他用户线程在运行,那么就没有可服务对象,也就没有理由继续下去。(操作系统里面是没有所谓的守护线程的概念,只有守护进程一说,但是Java语言机制是构建在JVM的基础之上的,意思是Java平台把操作系统的底层给屏蔽起来,所以它可以在它自己的虚拟的平台里面构造出对 自己有利的机制,而语言或者说平台的设计者多多少少是受到Unix思想的影响,而守护线程机制又是对JVM这样的平台凑合,于是守护线程应运而生);
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。
守护线程与用户线程的唯一区别是:其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开,当JVM中所有的线程都是守护线程的时候,JVM就可以退出了(如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了);如果还有一个或以上的非守护线程则不会退出。(以上是针对正常退出,调用System.exit则必定会退出)。
举个例子:就像天上人间的保安 (守护线程),里面有牌位姑娘(非守护线程),他们是可以同时干着各自的活儿,但是 姑娘们要是都被JC带走了,那么门口的保安也就没有存在的意义了。
创建守护线程
setDaemon() 将该线程标记为守护线程或者用户线程(后台线程)
setDaemon(true);
注: 1.该方法必须在启动线程前调用
2.当正在运行的线程都是守护线程时(前台线程全部结束),Java 虚拟机退出(自动)。但是不是立即停止。
线程常用方法
方法 | 说明 |
---|---|
public final void join()throws InterruptedException | 等待该线程终止,再运行其他线程 |
public final void setPriority(int newPriority) | 修改线程的优先级(1~10) |
public static void yield() | 暂停执行当前线程的对象,并执行其他线程 |
public void suspend() | 是线程进入阻塞状态,并且不会自动恢复,必须使用resume()方法,才能重新进入可执行状态(suspend容易造成死锁,已经过时) |
注:join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。
//Thread Joint用法
public class JoinDemo {
public static void main(String []a) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("join");
}
});
thread.start();
thread.join(); //join的用法是将该线程加入到当前线程中,并合并为顺序执行
System.out.println("main running");
}
}
输出:
join
main running
Jdk1.5后的改进
Lock
Jdk1.5以后将同步和锁封装成了对象,并将锁的隐式方法定义到对象中。
Lock接口:替代同步代码块或者同步函数,将同步的隐式锁操作变成了实锁操作。同时更为灵活,可以一个锁加上多组监视器。
lock():获取锁
unlock() :释放锁(通常定义在finally代码中)
Condition接口:替代Object中的wait notify notifyAll 方法。将这些监视器方法单独进行封装,变成Condition监视器对象。可以和任意的锁进行组合;
await()
signal()
signalAll()
class Resource {
private String name;
private int count = 1;
private Boolean flag = false;
Lock lock = new ReentrantLock(); //创建一个锁对象
//通过已有的锁获取该锁上的监视器对象(两组)
Condition producer_con = lock.new Condition();
Condition consumer_con = lock.new Condition();
public void set(String name) {
lock.lock();
try {
while (flag) {
try {
producer_con.await();
} catch (InterruptedException e) {
}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "生产者1.5" + this.name);
flag = true;
consumer_con.signal();//notifyAll();//防止死锁//会出现锁死的情况
} finally {
lock.unlock();
}
}
}
Callable
简单的任务没有返回值,如果主线程需要获得该线程的返回值,可以通过实现Callable接口,线程池执行任务并通过Future的实例返回线程的执行结果。
import java.util.concurrent.Callable;
public class MyCallable implements Callable{
//要执行的方法
//有返回值
@Override
public String call() throws Exception {
//线程阻塞一秒钟
Thread.sleep(1000);
return "callable 任务:线程阻塞一秒钟";
}
}
ThreadLocal
TLS(Thread local storage),线程局部存储,解决多线程中对同一变量的访问冲突的一种技术
ThreadLocal(线程本地变量)并没有继承Thread,也没有实现Runnable。
ThreadLocal类为每一个线程都维护了自己独有的变量拷贝,每个线程都拥有了自己独立一个变量。
重要的作用不在于多线程之间的数据共享,而是数据的独立。
由于每个线程在访问该变量的时候,读取和修都是在自己独有的那一份变量拷贝,不会被其他线程访问。
ThreadLocal中定义了一个哈希表用于为每个线程提供一个变量的副本。
可重用锁
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
synchrozied与lock的区别?
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;(Lock必须手动的释放锁)
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。(使用tryLock();方法,返回boolean)
5)Lock可以提高多个线程进行读操作的效率。
注:在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
管道
管道实际上是一种固定大小的缓冲区,管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。
它类似于通信中半双工信道的进程通信机制,一个管道可以实现双向 的数据传输,而同一个时刻只能最多有一个方向的传输,不能两个方向同时进行。
管道的容量大小通常为内存上的一页,它的大小并不是受磁盘容量大小的限制。当管道满时,进程在 写管道会被阻塞,而当管道空时,进程读管道会被阻塞。