文章目录
一、初始多线程
线程和进程
进程是程序向操作系统申请资源的基本单位,线程是进程中可独立执行的最小单位。通常一个进程可以包含多个线程,至少包含一个线程,同一个进程中所有线程共享该进程的资源。
- 1、根本区别
进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。 - 2、资源开销
每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间(在Java中,线程之间是共享堆区和方法区的资源,但是它们拥有着独立的栈区),每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。 - 3、包含关系
一个进程里可以包含多个线程。 - 4、内存分配
同一进程的线程共享本进程的地址空间和资源,而线程之间的地址空间和资源是相互独立的。 - 5、影响关系
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。 - 6、执行过程
每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
线程并发、并行、串行
- 并发
多个任务在同一个CPU核上,按细分的时间片轮流执行,从逻辑上来看任务是同时执行。 - 并行
单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。 - 串行
有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
二、创建线程
这里讲解创建线程的三种方式。
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
继承Thread类
使用方法:
- 创建一个继承Thread类的类。
- 重写Thread类中的run()方法。
- 用该类创建线程。
- 通过start()方法启动线程。
代码如下:
//创建线程的第一种方式:编写继承Thread的线程类。 package thread; class myThread extends Thread{ public void run(){ for(int i = 0; i < 1000; i++){ System.out.println("分支线程->" + i); } } } public class ThreadTest01 { public static void main(String[] args) { myThread t = new myThread(); t.start(); } }
这种方式现在不是很推荐,好的Java程序应该将并行运行的任务与运行机制解耦合。
实现Runnable接口
使用方法:
- 创建一个实现(implements)了Runnable接口的类。
- 实现Runnable接口中的run()方法。
- 创建Thread类对象,将实现了Runnable接口的类对象通过参数的形式传进去。
- 通过strat()启动线程。
代码如下:
public class ThreadTest02 { public static void main(String[] args) { Thread t = new Thread(new MyRunable()); t.start(); } } class MyRunable implements Runnable{ public void run(){ for(int i = 0; i < 1000; i++){ System.out.println("分支线程->" + i); } } }
该类较常用,因为Java程序要面向接口编程,实现了接口的类还可以继承其他类,而继承了类的类,不能在继承其他类。
实现Callable接口
使用步骤:
- 创建实现Callable接口的类;
- 以Callable接口的实现类为参数,创建FutureTask对象;
- 将FutureTask作为参数创建Thread对象;
- 调用线程对象的start()方法。
代码如下:
public class ThreadTest09 { public static void main(String[] args) { FutureTask futureTask = new FutureTask(new myCallable()); Thread thread = new Thread(futureTask); thread.start(); } } class myCallable implements Callable<Integer>{ public Integer call(){ System.out.println("Call----->start"); try { Thread.sleep(1000*3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Call----->end"); return 1000; } }
使用该方法创建线程时,核心方法是call(),与其他方式最大的不同是:该方法有返回值,其返回值类型就是Callable接口中泛型对应的类型
。我们通过FutureTask对象
调用get()
方法,获得该线程的返回值。但是这个方法也有相应的缺点,调用get()方法后,在主线程获得该线程的返回值前,主线程会进入阻塞状态。
注意:
调用get()方法会抛出两个异常:InterruptedException
、ExecutionException
三、线程的生命周期
线程的6种状态
Java中有如下6种状态
- New(新创建)
- Runnable(可运行)
- Blocked(阻塞)
- Waiting(等待)
- Timed waiting(计时等待)
- Terminated(被终止)
- 新建状态 :
使用new关键字创建一个thread对象,刚刚创建出的这个线程就处于新建状态。在这个状态的线程没有与操作系真正的线程产生关联,仅仅是一个java对象。 - 可运行:
正在进行运行的线程,只有处于可运行状态的线程才会得到cpu资源。
可运行状态可以分为两类理解:就绪状态和运行状态:
运行状态:拥有抢夺CPU时间片的能力,但还未抢夺成功。
就绪状态:成功抢夺CPU时间片。 - 阻塞 :
在可运行阶段争抢锁失败的线程就会从可运行—>阻塞 - 等待 :
可运行状态争抢锁成功,但是资源不满足,主动放弃锁(调用wait()方法)。条件满足后再恢复可运行状态(调用notiy()方法)。 - 有时限等待:
类似于等待,不过区别在于有一个等待的时间,到达等待时间后或者调用notiy(),都能恢复为可运行状态。
有两种方式可以进入有时限等待:wait(Long)和sleep(Long) - 终结 :代码全部执行完毕后,会进入到终结状态,释放所有的资源。
线程状态图
四、中断线程
中断线程作用
Java中可以调用interrupt()
向线程发出中断请求,从而使线程中断,中断不会对处于运行状态的线程产生影响,但是可以打断线程的阻塞状态(wait(), sleep()
),使其进入就绪状态,并抛出InterruptedException
异常。
测试代码如下:
public class ThreadTest06 { public static void main(String[] args) { Thread t = new Thread(new myRunnable2()); t.start(); t.interrupt(); } } class myRunnable2 implements Runnable{ public void run(){ System.out.println(Thread.currentThread().getName() + "------>start"); try { Thread.sleep(1000*1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "------>end"); } }
执行结果如下:
中断线程原理
每一个线程都有一个boolean
类型的中断状态,该中断状态初始为false
。当一个线程调用interrupted
方法时,线程的中断状态被置为true
。对于一个运行状态的线程来说,没有什么影响,但是对于阻塞状态的线程来说,遇到中断状态时(true),会抛出一个InterruptedException
,同时将中断状态重新置为false
。
测试代码如下:
public class ThreadTest06 { public static void main(String[] args) { Thread t = new Thread(new myRunnable2()); t.start(); } } class myRunnable2 implements Runnable{ public void run(){ System.out.println(Thread.currentThread().getName() + "------>start"); System.out.println("中断前:" + Thread.currentThread().isInterrupted()); Thread.currentThread().interrupt(); System.out.println("中断后(遇到堵塞之前):" + Thread.currentThread().isInterrupted()); System.out.println(); try { Thread.sleep(1000*1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("中断后(遇到堵塞之后):" + Thread.currentThread().isInterrupted()); System.out.println(Thread.currentThread().getName() + "------>end"); } }
运行结果如下:
关于中断的相关方法
- public void interrupt()
向线程发出中断请求,线程的中断状态被置为true。 - public static boolean interrupted()
测试当前线程(正在执行的线程)是否被中断,并将中断状态重置为false。 - public boolean isInterrupted()
测试线程的中断状态,但是并不会重置中断状态。
五、线程的属性
这里会对线程优先级和守护线程进行说明。
线程优先级
Java线程的优先级属性本质上只是一个给线程调度器的提示信息,以便于线程调度器决定优先调度哪些线程运行,也可以理解为,优先级高的线程更容易抢夺到CPU时间片。
每个线程的优先级都在1到10之间,1的优先级为最低,10的优先级为最高,在默认情况下优先级都是Thread.NORM_PRIORITY(常数 5)。
虽然开发者可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。
常用方法和属性
- static int MIN_PRIORITY
线程的最小优先级,最小优先级为1。 - static int MAX_PRIORITY
线程的最大优先级,最大优先级为10。 - static int NORM_PRIORITY
线程的默认优先级,默认优先级为5。 - public final int getPriority()
获得线程的优先级。 - public static void yield()
让当前线程处于让步状态,放弃CPU时间片,回到就绪状态,重新抢夺CPU时间片。 - public final void setPriority(int newPriority)
设置线程的优先级,但是必须在允许的范围内。
守护线程
Java 中的线程分为两种:守护线程和用户线程
。任何线程都可以设置为守护线程和用户线程,通过方法setDaemon(true)
可以把该线程设置为守护线程,反之则为用户线程。
用户线程:
运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。
守护线程:
运行在后台,为其他前台线程服务,比如垃圾回收线程,JIT(编译器)线程就可以理解为守护线程。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。
守护线程应该永远不去访问固有资源 ,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
注意事项:
- setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException。
- 在守护线程中产生的新线程也是守护线程。
- 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑。
- 守护线程中不能依靠finally块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作,所以守护线程中的finally语句块可能无法被执行。
相关方法
public final void setDaemon(boolean on)
将线程设置为守护线程。
六、线程同步
- 同步
多个线程访问共享资源时,只有当一个线程访问结束后其它线程才能继续方法。 - 异步
多个线程即使在访问共同资源时也互不影响,各自完成各自的任务。
线程竞争产生的风险
当两个线程存取相同对象,并且每一个线程都调用了修改线程状态的方法,将会发生什么呢?
下面用代码演示:
首先创建一个银行账户类,如下:
public class Account { //账号 private String actno; //余额 private int banlance; //构造器 public Account() { } public Account(String actno, int banlance) { this.actno = actno; this.banlance = banlance; } //setter and getter public void setBanlance(int banlance) { this.banlance = banlance; } public int getBanlance() { return banlance; } //取钱操作 public void takeOut(Account act, int takeOut){ int newBanlance = act.getBanlance() - takeOut; //这里sleep()只是为了让效果更加明显 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } act.setBanlance(newBanlance); System.out.println( Thread.currentThread().getName() + "取出:" + takeOut + " 剩余:" + newBanlance); } }
然后我们实现一个Runnable类,如下:
class myRunnable implements Runnable{ //银行账户对象 Account act; //要取出的金额 int takeOut; //构造器 public myRunnable(){ } public myRunnable(Account act, int takeOut){ this.act = act; this.takeOut = takeOut; } public void run(){ act.takeOut(act,takeOut); } }
然后我们用两个线程同时对一个账户进行取钱操作,如下:
class Test{ public static void main(String[] args) { Account act = new Account("Joken",10000); //去五次钱,每次取五千 Thread t1 = new Thread(new myRunnable(act,5000)); Thread t2 = new Thread(new myRunnable(act,5000)); t1.start(); t2.start(); } }
程序运行结果如下:
取了两次钱,但是余额仍然还有5000。
对产生风险的解释
产生以上问题的原因在于:act.takeOut(act,takeOut)并不是一个原子操作
这条指令会被分解为:
- int newBanlance = act.getBanlance() - takeOut;
计算出处取钱后剩余的余额。 - act.setBanlance(newBanlance);
更新余额到对象账户种。 - 打印账户余额信息。
注意: 在这里我并未将每一个操作都分解为原子操作,因为这里的样例线程较少,操作也很少,为了方便讲解所以将风险忽略,但是在实际开发中并不能忽略。
现在,线程一执行完了第一步,然后他被剥夺了CPU执行权,线程二又开始执行第一步,虽然线程一取了5000元,但是还没来得及更新,所以线程二在执行时,账户对象的余额仍然是10000元,这样就产生了错误。
synchronized关键字实现线程同步
- synchronized关键字是用来控制线程同步的。在多线程的环境下,synchronized控制的代码段不被多个线程同时执行,以达到保证并发安全的效果。
- synchronized关键字实现的锁称为内部锁,内部锁是一种排他锁,能够保证原子性、可见性和有序性。之所以被称为内部锁,是因为线程对内部锁的申请与释放的动作都是由Java虚拟机负责实现的,开发者看不到这个锁的获取和释放过程。
将上部分代码通过synchronized改进,如下:
public void takeOut(Account act, int takeOut){ synchronized (this){ int newBanlance = act.getBanlance() - takeOut; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } act.setBanlance(newBanlance); System.out.println( Thread.currentThread().getName() + "取出:" + takeOut + " 剩余:" + newBanlance); } }
对以上代码的解释:
- synchronized (this){ … },意味着想要执行花括号中的代码,首先要在锁池中找到this的对象锁,
找到锁后该线程才会进入可运行状态,没找到锁他就会进入阻塞状态,直到它在锁池中找到这个对象锁。(其它线程将这个锁释放)
。 - 两个线程同时对一个账户对象进行取钱操作,其中一个线程会先到账户对象锁,另一个线程找不到锁就会进入阻塞状态,当获得对象锁的线程将所有指令完成之后就会归还账户对象锁,处于阻塞状态的线程就会进入可运行状态。
**synchronized的三种用法 **
用法一:
synchronized(共享对象){ //同步代码 }
用法二:
synchronized 加在实例方法上,锁的是this,同步整个方法体。
public synchronized void test(){......}
synchronized 加在静态方法上,找的是类锁,一个类无论new几个对象都只有一个类锁。
用法三:
public static synchronized void test(){......}
七、死锁
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
死锁样例:
首先创建两个线程类,如下:
class myThread extends Thread{ Object o1; Object o2; public myThread(Object o1, Object o2){ this.o1 = o1; this.o2 = o2; } public void run(){ synchronized (o1){ System.out.println(Thread.currentThread().getName() + "拿到o1的锁,准备拿o2的锁"); synchronized (o2){ System.out.println(Thread.currentThread().getName() + "拿到o2的锁"); } } } } class myThread2 extends Thread{ Object o1; Object o2; public myThread2(Object o1, Object o2){ this.o1 = o1; this.o2 = o2; } public void run(){ synchronized (o2){ System.out.println(Thread.currentThread().getName() + "拿到o2的锁,准备拿o1的锁"); synchronized (o1){ System.out.println(Thread.currentThread().getName() + "拿到o1的锁"); } } }
创建这两个线程类对象,并传入相同对象,如下:
public class deadLock { public static void main(String[] args) { Object o1 = new Object(); Object o2 = new Object(); myThread t1 = new myThread(o1,o2); myThread2 t2 = new myThread2(o1,o2); t1.start(); t2.start(); } }
运行结果如下:
八、简单实现生产消费者问题
生产消费者问题是多线程经典问题,其中生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据
相关方法
- void notify()
随机选择在该对象上调用wait方法的线程,解除其阻塞状态。该方法只能在一个同步方法或同步代码块中调用。如果当前线程不是该对象锁的持有者,该方法将会抛出一个IllegalMonitorStateException
。 - void notifyAll()
解除在该对象上调用wait方法的全部线程,解除其阻塞状态。该方法只能在一个同步方法或同步代码块中调用。如果当前线程不是该对象锁的持有者,该方法将会抛出一个IllegalMonitorStateException
。 - void wait()
使当前线程进入阻塞状态,并释放该对象的锁,直到它被通知(nitify),并且在调用时抛出InterruptedException
,如果当前线程不是该对象锁的持有者,该方法将会抛出一个IllegalMonitorStateException
。
代码实现
public class ThreadApplication { public static void main(String[] args) { List<String> list = new ArrayList<>(); ConsumerThread consumer = new ConsumerThread(list); ProcuderThread procuder = new ProcuderThread(list); consumer.setName("消费者"); procuder.setName("生产者"); consumer.start(); procuder.start(); } } //消费者线程 class ConsumerThread extends Thread{ List<String> list = new ArrayList<>(); public ConsumerThread(List<String> list){ this.list = list; } public void run(){ while(true){ synchronized (list){ if(list.isEmpty()){ try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "消费了" + list.remove(0)); list.notify(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } } //生产者线程 class ProcuderThread extends Thread{ int i = 1; List<String> list = new ArrayList<>(); public ProcuderThread(List<String> list){ this.list = list; } public void run(){ while(true){ synchronized (list){ if(list.size() == 10){ try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.add("产品" + i); System.out.println(Thread.currentThread().getName() + "生产了" + list.get(list.size() - 1)); i++; list.notify(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } }
编译结果如下: