1、并发与并行
- 并行:多个任务在同一个时刻点同时执行。效率高
- 并发:多个任务在同一段时间内分时执行。效率低,宏观上同时执行,微观上分时执行。
2、进程与线程
- 进程:内存中正在运行的应用程序,是系统进行资源分配和调度的基本单位。每一个进程有自己独立的运行空间,相互之间不影响。进程就是程序的一次执行过程,即是一个进程从加载到内存到从内存中释放消亡的过程。
- 线程:进程内部的独立运行单元,是操作系统能够进行运算调度的最小单位,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
- 多线程: 在一个进程中,可以同时开启多个线程,让多个线程同时去执行某些任务(功能)。多线程的目的是提高程序的运行效率。
- 主线程:任何一个应用程序的运行,都有一个独立的运行入口。而负责这个入口的线程称为程序运行的主线程。Java程序的主线程即main线程。
3、线程的调度
1. Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
2. 由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。
3. 因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。
线程的调度方式分两类: 分时调度 , 抢占式调度(时间片轮转)。
分时调度:多个任务平均分配执行时间。
抢占式调度:线程之间抢夺CUP的执行权,谁抢到谁执行(随机性)。
java.lang.Thread: 表示线程,是程序中执行的线程。 Java虚拟机允许应用程序同时执行多个执行线程。
1、多线程的创建
创建一个新的执行线程有两种方法。
- 创建子类,继承Thread类
- 实现Runnable接口
- 第一种方式
- 声明Thread的子类,声明一个类继承Thread。
- 在子类中重写run()方法,即线程任务。
- 创建子类对象。
- 启动多线程。
- 成员方法
void run•() 线程任务方法,需要多线程执行的代码都写在此方法内。 void start•() 开启新的线程,java虚拟机调用此线程的run方法。
声明 Thread 子类
// 声明Thread子类
public class MyThread extends Thread {
// 重写run()方法,线程任务
@Override
public void run() {
for( int i = 1; i <= 20; i++ ) {
System.out.println("旺财..." + i );
}
}
}
测试代码
public class Demo {
public static void main(String[] args) {
// 创建子类对象
MyThread mt = new MyThread();
// 启动线程任务
// mt.run(); 直接调用run()方法无法开启新的线程
mt.start(); // 开启新线程并调用run()方法
for( int i = 1; i <= 20; i++ ) {
System.out.println("小强...." + i );
}
}
}
调用run()方法 和 start()方法的区别
2、线程名
- 多线程对象在创建出来之后,都有默认的线程名,命名规则: Thread-x , 从0开始,逐一增加。我们可以通过方法给线程命名,同样也可以获取线程的名字。
- 构造方法给线程命名
Thread(String name) 分配新的 Thread 对象,并命名。
- 成员方法给线程命名
void setName•(String name) 将此线程的名称更改为等于参数 name 。
- 获取线程名
String getName•() 返回此线程的名称。
- 获取线程对象
static Thread currentThread•() 返回对当前正在执行的线程对象的引用。
线程代码演示
public class ThreadName extends Thread {
// 构造方法
public ThreadName() {
}
public ThreadName(String name) {
super(name);
}
// 重写run()方法,线程任务
@Override
public void run() {
for( int i = 1; i <= 20; i++ ) {
System.out.println(getName() + ".." + i );
}
}
}
测试代码演示
public class Demo {
public static void main(String[] args) {
// 创建线程对象一
ThreadName tn1 = new ThreadName("旺财");
// 获取线程的名字
System.out.println( tn1.getName() );
// 创建线程对象二
ThreadName tn2 = new ThreadName();
// 设置线程名字
tn2.setName("来福..");
// 获取线程的名字
System.out.println(tn2.getName());
// 启动线程
tn1.start();
tn2.start();
// 主线程
for( int i = 1; i <= 20; i++ ) {
System.out.println(Thread.currentThread().getName() + i );
}
}
}
3、线程优先级
- 每个线程都有优先级,具有较高优先级的线程优先于优先级较低的线程执行。线程的优先级范围是从 1- 10,默认的优先级是5。
优先级成方法
- int getPriority•() 返回此线程的优先级。
- void setPriority•(int newPriority) 更改此线程的优先级。
线程代码演示
public class ThreadPriority extends Thread {
// 构造方法
public ThreadPriority() {
}
public ThreadPriority(String name) {
super(name);
}
// 线程任务
@Override
public void run() {
for ( int i = 1; i <= 20; i++ ) {
System.out.println( getName() + ".." + i );
}
}
}
测试代码演示
public class Demo {
public static void main(String[] args) {
// 创建线程任务
ThreadPriority tp1 = new ThreadPriority("tp1.");
ThreadPriority tp2 = new ThreadPriority("tp2..");
ThreadPriority tp3 = new ThreadPriority("tp3...");
// 设置线程优先级 tp2为默认优先级
tp1.setPriority(1);
tp3.setPriority(10);
// 获取优先级
System.out.println( tp1.getPriority() );
System.out.println( tp2.getPriority() );
System.out.println( tp3.getPriority() );
// 启动线程
tp1.start();
tp2.start();
tp3.start();
}
}
4、守护线程
API中有这样的描述,每个线程可能也可能不会被标记为守护程序,并且当且仅当创建线程是守护进程时才是守护线程。
线程分为用户线程和守护线程两种
- 用户线程:也就是普通线程,刚创建出来的线程都属于用户线程。
- 守护线程:顾名思义,守护线程是用来守护的,专门用于服务用户线程,当程序中没有正在运行的用户线程时,守护线程会自动结束。垃圾回收线程就是典型的守护线程。
守护线程方法
- void setDaemon•(boolean on) 将此线程标记为 daemon线程或用户线程。
- boolean isDaemon•() 测试这个线程是否是守护线程。
线程代码演示
public class ThreadDaemon extends Thread {
// 线程任务
@Override
public void run() {
for ( int i = 1; i <= 200; i++ ) {
System.out.println( "守护线程.." + i );
}
}
}
测试代码演示
public class Demo {
public static void main(String[] args) {
// 创建线程对象
ThreadDaemon td = new ThreadDaemon();
// 将线程设置为守护线程
td.setDaemon(true);
// 启动线程
td.start();
/*
主线程循环
此时主线程就是一个用户线程,主线程循环20次,
而上面的守护线程循环200次,注意当主线程执行
结束之后,守护线程的执行数据
*/
for( int i = 1; i <=20;i++ ) {
System.out.println("main.." + i );
}
}
}
5、线程休眠
休眠方法
- static void sleep•(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数,这取决于系统定时器和调度程序的精度和准确性。
代码演示
public class Demo {
public static void main(String[] args) throws InterruptedException {
// 使用线程休眠方法 制作一个闹钟
for( int i = 1;i <= 10; i++ ) {
Thread.sleep(1000);
System.out.println("第" + i + "秒");
}
System.out.println("叮铃铃...");
}
}
- java.lang.Runnable: 线程任务接口,该接口应由任何类实现,其实例将由线程执行。 类必须定义一个无参数的方法,称为run 。
多线程的第二种方式
1. 声明Runnable接口的实现类
2. 实现类中重写run()方法
3. 创建实现类对象
4. 创建Thread线程对象,并将实现类对象传递给构造方法。
5. 启动线程
Runnable实现类
public class MyRunnable implements Runnable {
// 线程任务
@Override
public void run() {
for( int i = 1; i <= 20; i++ ) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
测试代码演示
public class Demo {
public static void main(String[] args) {
// 创建线程任务对象
MyRunnable mr = new MyRunnable();
// 创建线程对象
Thread thread1 = new Thread(mr);
Thread thread2 = new Thread(mr);
// 启动线程
thread1.start();
thread2.start();
}
}
2、线程两种创建方式的区别
打破了Java的单继承性
- Java具备单继承性,一个子类只能继承一个父类。
- 多线程的第一种创建方式,需要继承Thread类。如果此时这个类已经继承了其他的父类,就无法继承Thread类。如果改为继承Thread类就改变了这个类的当前继承体系。
- 多线程的第二种创建方式,不需要类采用继承的方式实现,在实现Runnable接口的同时不会影响到类原有的继承体系。
实现类解耦
- 多线程的第一种创建方式,线程对象和线程任务是直接耦合在一起的。
- 多线程的第二种创建方式,实现了线程对象和线程任务的解耦。线程对象专门用来对线程本身进行操作,线程任务单独抽取到Runnable接口中独立操作。当一个类实现了Runnable接口,就相当于有了线程任务,创建Thread对象拿到线程任务就可以执行,达到了线程任务和线程对象的分离及结合。
第一种方式实现匿名线程
public class Demo {
public static void main(String[] args) {
// 创建Thread匿名子类
new Thread(){
// 线程任务
@Override
public void run() {
System.out.println("线程启动...");
}
}.start();
}
}
第二种方式实现匿名线程
public class Demo {
public static void main(String[] args) {
// 创建Thread匿名子类 传递匿名Runnable对象
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程启动...");
}
}).start();
}
}
练习:就近原则,离的近的调用方法
public class Demo {
public static void main(String[] args) {
// 运行哪段代码
new Thread(new Runnable() {
// 匿名Runnable对象
@Override
public void run() {
System.out.println("Runnable匿名实现类..线程启动...");
}
}){
// Thread匿名子类
@Override
public void run() {
System.out.println("Thread匿名子类类..线程启动...");
}
}.start();
}
}
1、线程的安全分析
- 有时候我们需要使用多线程操作共享的数据,在操作共享数据的过程中特别容易发生线程的安全问题。下面我们通过一个案例来看一下线程安全问题发生的原因。
案例
- 使用多线程模拟火车站售票,每一条线程相当于一个售票的窗口。而所有窗口所售的票是共享的,也就是说当有一个窗口把某张票卖出去之后,其他的窗口也就不能在卖那张票。窗口可以有多个,但是每一张票是唯一的。
线程任务
public class Ticket implements Runnable {
// 声明变量 模拟车票
private int num = 100;
// 线程任务就是售票
@Override
public void run() {
// 死循环模拟窗口在一直售票
while ( true ) {
// 判断是否还有车票
if( num > 0 ) {
// 获取线程名字
String name = Thread.currentThread().getName();
System.out.println( name + "售票:" + num );
// 线程休眠,模拟出票时间
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票数迭代,减去售出的票
num--;
}
}
}
}
测试类
public class Demo {
public static void main(String[] args) {
// 创建线程任务对象
Ticket ticket = new Ticket();
// 创建线程对象 模拟售票窗口
Thread t1 = new Thread(ticket,"窗口一");
Thread t2 = new Thread(ticket,"窗口二");
Thread t3 = new Thread(ticket,"窗口三");
Thread t4 = new Thread(ticket,"窗口四");
// 启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果
原因分析
- 通过程序的运行结果可以看到,有些票被多次卖出,而有些票没有被卖出而是被覆盖掉。发生问题的原因是多线程在执行售票任务的时候,操作了共享的成员变量num。可是在操作num的过程中,一个线程操作到其中一部分代码的时候,CPU切换到其他线程开始执行线程任务,这样就导致了num变量的值被修改的不一致。
类似于上述的这些问题,我们称为线程的安全问题。而造成多线程安全问题的原因:
- 有多条线程
- 多个线程在操作共享的数据
- 操作共享数据的语句不止一条,并且对共享数据有修改
- 本质的原因是CPU在处理多个线程的时候,在操作共享数据的多条代码之间切换导致的,一条线程的线程任务还没有直接结束,就切换到了另外一条线程。
解决方案
- 上述的问题分析我们知道,造成安全的原因的CPU的随机切换造成的,但是CPU是由操作系统控制的,我们无法直接干预CPU的切换,所以只能从线程本身入手。
解决方案:我们可以人为的控制,当有一条线程在执行操作共享数据的代码时,不让其他线程进入到操作共享数据的代码中。只有当某条线程将操作共享数据的代码执行结束,其他线程才可以继续执行操作共享语句的代码。这样就可以保证线程的安全。
上述的解决方案,称为线程的同步。实现线程同步的方式有3种:
- synchronized同步代码块
- 同步方法
- Lock锁机制
2、同步代码块
语法:
synchronized (任意唯一锁对象) { 操作共享数据的代码; }
public class Ticket implements Runnable {
// 声明变量 模拟车票
private int num = 100;
// 声明锁对象
private Object lock = new Object();
// 线程任务就是售票
@Override
public void run() {
// 死循环模拟窗口在一直售票
while ( true ) {
// 同步代码块
synchronized ( lock ) {
// 判断是否还有车票
if (num > 0) {
// 获取线程名字
String name = Thread.currentThread().getName();
System.out.println(name + "售票:" + num);
// 线程休眠,模拟出票时间
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票数迭代,减去售出的票
num--;
}
}
}
}
}
3、同步方法
当在run()方法中调用了其他方法时,那么被调用的方法也间接变为了线程任务。
同步方法,就是使用synchronized关键字修饰的方法,当某个方法中的代码全部是操作共享数据的代码时,我们可以直接将当前方法声明为同步方法。
语法
public synchronized void methodName( 形参 ) { 操作共享数据的代码; }
public class Ticket implements Runnable {
// 声明变量 模拟车票
private int num = 100;
// 线程任务就是售票
@Override
public void run() {
// 死循环模拟窗口在一直售票
while ( true ) {
ticket();
}
}
// 声明一个专门售票的方法
public synchronized void ticket() {
// 判断是否还有车票
if (num > 0) {
// 获取线程名字
String name = Thread.currentThread().getName();
System.out.println(name + "售票:" + num);
// 线程休眠,模拟出票时间
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票数迭代,减去售出的票
num--;
}
}
}
注意事项
- 只有方法中的所有代码都是操作共享数据的代码时,才使用synchronized声明为同步方法,否则会降低执行效率。
- run()方法不能使用synchronized修饰,否则线程任务无法被多线程执行。
4、继承实现售票案例
继承的方式不利于数据的共享,所以需要考虑的如何实现数据的共享问题。
public class Ticket extends Thread {
// 声明变量 模拟车票 需要静态,保证车票被共享
private static int num = 100;
// 声明锁对象 需要静态,保证锁唯一
private static Object lock = new Object();
// 构造方法给线程命名
public Ticket(String name) {
super(name);
}
// 线程任务就是售票
@Override
public void run() {
// 死循环模拟窗口在一直售票
while ( true ) {
// 同步代码块
synchronized ( lock ) {
// 判断是否还有车票
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "售票:" + num);
// 模拟出票时间
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 迭代车票
num--;
}
}
}
}
}
测试类
public class Demo {
public static void main(String[] args) {
// 创建线程对象 模拟窗口
Ticket ticket1 = new Ticket("窗口一");
Ticket ticket2 = new Ticket("窗口二");
Ticket ticket3 = new Ticket("窗口三");
Ticket ticket4 = new Ticket("窗口四");
// 启动线程
ticket1.start();
ticket2.start();
ticket3.start();
ticket4.start();
}
}
5、同步的细节
当程序中加了同步之后,依然存在线程的安全问题,那么原因有如下两个
- 锁不唯一
- 同步代码块没有加在所有操作共享数据的代码上
同步的好处和弊端:
- 使用同步会影响程序的执行效率。每次CPU运行到同步代码时,都需要去判断有没有线程在同步代码中,如果只能等待同步代码中的线程执行结束。
- 好处显而易见是可以保证数据的安全。
在前面学习的:
- StringBuffer它是线程安全的。提供的方法中有同步。只要有同步效率肯定会降低。
- StringBuilder它是线程不安全的。
在集合中学习的JDK1.2出现的所以有集合都是线程不安全,JDK1.2之前的都是线程安全的。
Vector,Hashtable。
如果开发时需要对集合进行线程的安全操作,这时需要使用Collections中的方法,把不安全的集合变成安全的集合。
单例设计模式:程序在运行的过程中只允许产生一个对象。
单例设计模式的实现过程一共3步:
- 私有构造方法。
- 本类创建对象。
- 提供公开静态获取本类对象的方法。
代码的实现分为懒汉式和饿汉式两种。
饿汉式
public class Single {
// 私有构造方法
private Single(){
}
// 本类创建对象
private static final Single s = new Single();
// 返回本类对象的方法
public static Single getInstance() {
return s;
}
}
懒汉式
public class Single {
// 私有构造方法
private Single(){}
// 本类创建对象
private static Single s = null;
// 返回本类对象的方法
public static Single getInstance() {
if ( s == null ) {
s = new Single();
}
return s; // 返回对象
}
}
- 上述的两种方式中,饿汉式的对象创建过程只有一条语句,不会发生线程的安全问题。但是懒汉式的对象创建不止一条语句,如果有多线程操作时,就会发生线程的安全问题。下面我们通过代码来验证一下,线程任务就是获取单例的对象,查看对象是否唯一。
线程任务
public class SingleThread implements Runnable {
// 线程任务就是获取单例对象
@Override
public void run() {
// 获取单例对象
Single instance = Single.getInstance();
// 打印对象
System.out.println(instance);
}
}
测试代码
public class Demo {
public static void main(String[] args) {
// 创建线程任务对象
SingleThread st = new SingleThread();
// 创建线程对象
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
Thread t3 = new Thread(st);
Thread t4 = new Thread(st);
// 启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果
- 通过多线程获取单例对象时发现并没有实现对象唯一,原因很简单,就是创建对象的语句有多条,而我们没有实现线程的同步。
线程同步:两次判断,第一次判断提高效率,第二次用来保证线程安全
public class Single {
// 声明锁对象
private static Object lock = new Object();
// 私有构造方法
private Single(){}
// 本类创建对象
private static Single s = null;
// 返回本类对象的方法
public static Single getInstance() {
/*
外出的判断可以提高程序的运行效率,当有一条线程创建完对象之后
其他线程就不会在进入到同步代码块中。
*/
if( s == null ) {
// 同步代码块
synchronized (lock) {
if (s == null) {
s = new Single();
}
}
}
// 返回对象
return s;
}
}
- 死锁:多个线程操作共享数据,但是要求线程获取锁的先后次序不同。但是都必须根据自己的次序获取所有的锁才能去执行这个任务。而在获取锁的过程中,不同的线程获取方式不同,导致锁会被其他的线程占有。一旦发生这个问题,就立刻发生死锁问题。
案例
- 有2个线程,需要执行相同的任务,但是需要分别获取的A和B锁才能去执行,第一个线程获取锁的顺序是先A后B。第二个线程获取锁的顺序是先B后A。
线程任务演示
public class DieThread implements Runnable{
// 声明AB两个锁对象
private Object lock_A = new Object();
private Object lock_B = new Object();
// 声明变量 控制执行流程
boolean flag = false;
@Override
public void run() {
if( flag ) {
while ( true ) {
// 获取A锁的同步代码块
synchronized (lock_A ) {
System.out.println("if..lock_A 锁...");
// 获取B锁的同步代码块
synchronized (lock_B) {
System.out.println("if..lock_B锁...");
}
}
}
} else {
while (true){
// 获取B锁的同步代码
synchronized (lock_B) {
System.out.println("else..lock_B锁...");
// 获取A锁的同步代码块
synchronized (lock_A) {
System.out.println("else..lock_A锁...");
}
}
}
}
}
}
测试演示
public class Demo {
public static void main(String[] args) throws InterruptedException {
// 创建线程任务对象
DieThread dt = new DieThread();
// 创建线程对象
Thread a = new Thread(dt);
Thread b = new Thread(dt);
// 启动线程任务
a.start();
// 线程休眠
Thread.sleep(1);
// 修改flag的值,让线程可以进入if语句
dt.flag = true;
// 启动线程
b.start();
}
}