并行(parallel):指多个事件任务在同一时刻发生(同时发生)。
并发(concurrency):指两个或多个事件在同一个微小的时间段内发生。程序并发执行可以在有限条件下,充分利用CPU资源。
单核CPU:只能并发
多核CPU:并行+并发
程序:为了完成某个任务和功能,选择一种编程语言编写的一组指令的集合。
软件:1个或多个应用程序+相关的素材和资源文件等构成一个软件系统。
进程是对一个程序运行过程(创建-运行-消亡)的描述,系统会为每个运行的程序建立一个进程,并为进程分配独立的系统资源,比如内存空间等资源。
线程:线程是进程中的一个执行单元,负责完成执行当前程序的任务,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这时这个应用程序也可以称之为多线程程序。多线程使得程序可以并发执行,充分利用CPU资源。
面试题:进程是操作系统调度和分配资源的最小单位,线程是CPU调度的最小单位。不同的进程之间是不共享内存的。进程之间的数据交换和通信的成本是很高。不同的线程是共享同一个进程的内存的。当然不同的线程也有自己独立的内存空间。对于方法区,堆中中的同一个对象的内存,线程之间是可以共享的,但是栈的局部变量永远是独立的。
指CPU资源如何分配给不同的线程。常见的两种线程调度方式:
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java采用的是抢占式调度方式。
通过继承Thread类来创建并启动多线程的步骤:
多线程执行情况分析
注意事项:
手动调用run方法不是启动线程的方式,只是普通方法调用。
start方法启动线程后,run方法会由JVM调用执行。
不要重复启动同一个线程,否则抛出异常IllegalThreadStateException
不要使用Junit单元测试多线程,不支持,主线程结束后会调用System.exit()
直接退出JVM;
Thread类本身也是实现了Runnable接口的,run方法都来自Runnable接口,run方法也是真正要执行的线程任务。
public class Thread implements Runnable {}
因为Java类是单继承的,所以继承Thread的方式有单继承的局限性,但是使用上更简单一些。
实现Runnable接口的方式,避免了单继承的局限性,并且可以使多个线程对象共享一个Runnable实现类(线程任务类)对象,从而方便在多线程任务执行时共享数据。
匿名内部类对象的方式创建线程,并不是一种新的创建线程的方式,只是在线程任务只需执行一次的情况下,我们无需单独创建线程类,可以采用匿名对象的方式:
new Thread("新的线程!"){@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName()+":正在执行!"+i);}}
}.start();new Thread(new Runnable(){@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+":" + i);}}
}).start();
public void run() :此线程要执行的任务在此处定义代码。
public String getName() :获取当前线程名称。
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。
public final int getPriority() :返回线程优先级
public final void setPriority(int newPriority) :改变线程的优先级
public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
public static void sleep(long millis) :线程睡眠,使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
public static void yield():线程礼让,yield只是让当前线程暂时失去执行权,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
void join() :加入线程,当前线程中加入一个新线程,等待加入的线程终止后再继续执行当前线程。
void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。
void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
public final void stop():强迫线程停止执行。 该方法具有不安全性,已被弃用,最好不要使用。
public void interrupt():中断线程,实际上是给线程打上一个中断的标记,并不会真正使线程停止执行。
public static boolean interrupted():检查线程的中断状态,调用此方法会清除中断状态(标记)。
public boolean isInterrupted():检查线程中断状态,不会清除中断状态(标记)
public void setDaemon(boolean on):将线程设置为守护线程。必须在线程启动之前设置,否则会报IllegalThreadStateException
异常。
public boolean isDaemon():检查当前线程是否为守护线程。
volatile的作用是确保不会因编译器的优化而省略某些指令,volatile的变量是说这变量可能会被意想不到地改变,每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份,这样,编译器就不会去假设这个变量的值了。
传统线程模型的五种线程状态
JDK定义的六种线程状态
在java.lang.Thread
类内部定义了一个枚举类用来描述线程的六种状态:
public enum State {NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;}
说明:当从WAITING
或TIMED_WAITING
恢复到Runnable
状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入BLOCKED
状态。
当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,但是如果多个线程中对资源有读和写的操作,就会出现前后数据不一致问题,这就是线程安全问题。
线程安全问题引出
总结:线程安全问题的出现因为具备了以下条件
同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。
public synchronized void method(){可能会产生线程安全问题的代码
}
同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。
synchronized(同步锁){需要同步操作的代码
}
首选this其次是类.class也可以是" "
空字符串对象
同步锁对象:
同步代码块的锁对象
锁的范围太小:不能解决安全问题,要同步所有操作共享资源的语句。
锁的范围太大:因为一旦某个线程抢到锁,其他线程就只能等待,所以范围太大,效率会降低,不能合理利用CPU资源。
1、饿汉式没有线程安全问题
饿汉式:上来就创建对象
2、懒汉式线程安全问题
public class Singleton {private static Singleton ourInstance;public static Singleton getInstance() {//一旦创建了对象,之后再次获取对象,都不会再进入同步代码块,提升效率if (ourInstance == null) {//同步锁,锁住判断语句与创建对象并赋值的语句synchronized (Singleton.class) {if (ourInstance == null) {ourInstance = new Singleton();}}}return ourInstance;}private Singleton() {}
}
在一个线程满足某个条件时,就进入等待状态(wait()/wait(time)), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify());或可以指定wait的时间,等时间到了自动唤醒;在有多个线程进行等待时,如果需要,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。
注意:
被通知线程被唤醒后也不一定能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
调用wait和notify方法需要注意的细节
1、释放锁的操作
当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。
当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁。
2、死锁
不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
3、面试题:sleep()和wait()方法的区别
(1)sleep()不释放锁,wait()释放锁
(2)sleep()指定休眠的时间,wait()可以指定时间也可以无限等待直到notify或notifyAll
(3)sleep()在Thread类中声明的静态方法,wait方法在Object类中声明
因为我们调用wait()方法是由锁对象调用,而锁对象的类型是任意类型的对象。那么希望任意类型的对象都要有的方法,只能声明在Object类中。
练习
要求两个线程,同时打印字母,每个线程都能连续打印3个字母。两个线程交替打印,一个线程打印字母的小写形式,一个线程打印字母的大写形式,但是字母是连续的。当字母循环到z之后,回到a。
public class PrintLetterDemo {public static void main(String[] args) {// 2、创建资源对象PrintLetter p = new PrintLetter();// 3、创建两个线程打印new Thread("小写字母") {public void run() {while (true) {p.printLower();try {Thread.sleep(1000);// 控制节奏} catch (InterruptedException e) {e.printStackTrace();}}}}.start();new Thread("大写字母") {public void run() {while (true) {p.printUpper();try {Thread.sleep(1000);// 控制节奏} catch (InterruptedException e) {e.printStackTrace();}}}}.start();}
}// 1、定义资源类
class PrintLetter {private char letter = 'a';public synchronized void printLower() {for (int i = 1; i <= 3; i++) {System.out.println(Thread.currentThread().getName() + "->" + letter);letter++;if (letter > 'z') {letter = 'a';}}this.notify();try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}public synchronized void printUpper() {for (int i = 1; i <= 3; i++) {System.out.println(Thread.currentThread().getName() + "->" + (char) (letter - 32));letter++;if (letter > 'z') {letter = 'a';}}this.notify();try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}
}