一、线程
1.1. 程序、进程和线程的概念
程序:为完成特定任,用某种语言编写的一组指令集合,指一段静态的代码;
进程:正在运行的一个程序,是资源分配的单位,是动态的;
线程:
①一个进程可以包含多个线程;
②是一个程序内部的一条执行路径;
③每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小;
④线程是调度和执行的单位;
⑤一个进程中的多个线程共享相同的内存单元/内存地址空间,可以访问相同的变量和对象
⑥若一个进程同一时间并行执行多个线程,就是支持多线程
一个Java应用程序,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
多线程程序的优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
1.2. 并行和并发
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
1.3. 线程的分类
- 一种是守护线程,一种是用户线程。
- 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
- 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
- Java垃圾回收就是一个典型的守护线程。
- 若JVM中都是守护线程,当前JVM将退出。
1.3. 线程的生命周期
新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
二、多线程
2.1. 创建线程的方式一:继承Thread
2.1.1. 基本步骤
- 定义一个继承 java.lang.Thread类的子类;
- 重写run()方法;
- 创建子类的对象,即线程;
- 用对象调用start(),即启动线程
2.1.2. 实例
//问题:使用两个线程分别打印100以内的偶数和奇数。
public class ThreadTest {
public static void main(String[] args) {
//3.创建继承Thread类的子类对象,即线程
OddNumber odd = new OddNumber();
EvenNumber even = new EvenNumber();
//给线程命名
odd.setName("odd线程");
even.setName("even线程");
//4.启动线程
odd.start();
even.start();
}
}
/**
* 打印100以内的奇数
*/
//1.定义继承java.lang.Thread类的子类
class OddNumber extends Thread {
//2.重写run()方法
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
/**
* 打印100以内的偶数
*/
//1.定义继承java.lang.Thread类的子类
class EvenNumber extends Thread {
//2.重写run()方法
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
2.2. 创建线程的方式二:实现Runnable
2.2.1. 基本步骤:
- 定义一个实现 java.lang.Runnable类的实现类;
- 实现run()方法;
- 创建实现类的对象;
- 将实现类的对象作为参数传到Thread(Runnable target)构造器中,创建Thread类的对象thread,即创建线程
- 用Thread类的对象调用start(),即启动线程;thread.start()
2.2.2. 实例
//问题:使用两个线程分别打印100以内的偶数和奇数。
public class ThreadTest {
public static void main(String[] args) {
//3.创建实现Runnable类的实现类对象
OddNumber odd = new OddNumber();
EvenNumber even = new EvenNumber();
//4.将实现Runnable类的实现类对象作为参数传给Thread(Runnable target)构造器,创建线程
Thread t1 = new Thread(odd);
Thread t2 = new Thread(even);
//给线程命名
t1.setName("odd线程");
t2.setName("even线程");
//5.启动线程
t1.start();
t2.start();
}
}
/**
* 打印100以内的奇数
*/
//1.定义实现java.lang.Runnable接口的实现类
class OddNumber implements Runnable {
//2.实现run()方法
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
/**
* 打印100以内的偶数
*/
//1.定义继承java.lang.Runnable类的子类
class EvenNumber implements Runnable {
//2.实现run()方法
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
2.3. 创建线程的方式三:实现Callable
2.3.1. 基本步骤:
- 定义一个实现 java.lang.Callable类的实现类;
- 实现call()方法;
- 创建实现类的对象;
- 将实现类的对象作为参数传到FutureTask(Callable target)构造器中,创建FutureTask类的对象futureTask,即创建线程
- 用FutureTask类的对象调用start(),即启动线程;futureTask.start()
- 获取实现类中call方法的返回值;Object callValue = futureTask.get()
2.3.2. 实例
//问题:使用两个线程分别打印100以内的偶数和奇数。
public class ThreadTest {
public static void main(String[] args) {
//3.创建实现Callable类的实现类对象
OddNumber odd = new OddNumber();
EvenNumber even = new EvenNumber();
//4.将实现Callable类的实现类对象作为参数传给FutureTask(Callable target)构造器,创建FutureTask对象
FutureTask<Integer> oddTask = new FutureTask<>(odd);
FutureTask<Integer> evenTask = new FutureTask<>(even);
//5.将FutureTask对象作为参数传给Thread(),创建线程
Thread t1 = new Thread(oddTask);
Thread t2 = new Thread(evenTask);
//给线程命名
t1.setName("odd线程");
t2.setName("even线程");
//6.启动线程
t1.start();
t2.start();
try {
//7.获取返回值,即100以内奇数和与100以内偶数和
Integer oddSum = oddTask.get();
Integer evenSum = evenTask.get();
System.out.println("奇数总和 = " + oddSum);
System.out.println("偶数总和 = " + evenSum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
/**
* 打印100以内的奇数,并计算其和
*/
//1.定义实现java.util.concurrent.Callable接口的实现类
class OddNumber implements Callable<Integer> {
//2.实现run()方法
@Override
public Integer call() {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0){
sum += i;
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
return sum;
}
}
/**
* 打印100以内的偶数,并计算其和
*/
//1.定义继承java.util.concurrent.Callable类的子类
class EvenNumber implements Callable<Integer> {
//2.实现run()方法
@Override
public Integer call() {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0){
sum += i;
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
return sum;
}
}
实现Runnable接口与实现Callable接口两种方式创建多线程的异同
所属包 | 有无返回值 | 是否可以抛异常 | 是否支持泛型 | 是否jdk5.0新增 | |
---|---|---|---|---|---|
Runnable | java.lang | 否 | 否 | 否 | 是 |
Callable | java.util.concurrent | 是 | 是 | 是 | 是 |
2.4. 创建线程的方式四:线程池
2.4.1. 使用线程池的优点
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
2.4.2. 实例
//问题:使用两个线程分别打印100以内的偶数和奇数。
public class ThreadTest {
public static void main(String[] args) {
//1.提供指定线程数的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
//2.执行指定线程
service1.execute(new OddNumber());
Future<Integer> evenTask = service1.submit(new EvenNumber());
try {
//3.获取实现Callable接口的实现类的返回值
Integer evenSum = evenTask.get();
System.out.println("偶数总和 = " + evenSum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}finally{
service1.shutdown();
}
}
}
/**
* 打印100以内的奇数
*/
//1.定义实现java.lang.Runnable接口的实现类
class OddNumber implements Runnable {
//2.实现run()方法
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
/**
* 打印100以内的偶数,并计算其和
*/
//1.定义继承java.util.concurrent.Callable类的子类
class EvenNumber implements Callable<Integer> {
//2.实现run()方法
@Override
public Integer call() {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0){
sum += i;
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
return sum;
}
}
2.5. 线程中常用方法
start() //启动线程,调用当前线程的 run()方法
setName(String name) //设置线程名称
getName() //获取当前线程名称
currentThread() //静态方法,返回执行当前代码线程
yield() //释放当前CPU的执行权
stop() //强制结束线程,已过时
suspend() //手动暂停当前线程,需要配合resume()使用
resume() //手动唤醒当前线程,需要配合suspend()使用
join() //在线程 a中调用线程 b的 join(),此时线程 a就进入阻塞状态,直到线程 b完全执行完以后,线程 a才结束阻塞状态。
sleep(long millitime) //让当前线程 "睡眠 "指定的 millitime毫秒
isAlive() //判断当前线程是否存活
getPriority() //获取线程的优先级。
setPriority(int p) //设置线程的优先级。
线程的优先级
最高优先级:MAX_PRIORITY = 10;
最低优先级:MIN _PRIORITY = 1;
默认优先级:NORM_PRIORITY = 5注意:高优先级的线程抢占cpu的执行权比低优先级线程抢占cpu的执行权的概率高,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
三、线程安全
3.1. 实现线程安全的三种方式
3.1.1. 同步代码块
1、实现规则
synchronized(同步监视器){
//需要被同步的代码
}
说明:
1.操作共享数据的代码,即为需要被同步的代码。不能包含代码多了,也不能包含代码少了。
2.共享数据:多个线程共同操作的变量。
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。要求:多个线程必须要共用同一把锁。
2、实例
//问题:加入有一个10000元的账户,丈夫和妻子两个人同时向这个账户分别取1000元,取两次
public class ThreadSynchronizd {
//方式一:继承Thread类
@Test
public void thread() {
//1.创建一个余额为10000的账户
Account acc = new Account(10000);
//2.创建两个线程,分别代表丈夫和妻子
PersonThread husband = new PersonThread(acc);
PersonThread wife = new PersonThread(acc);
husband.setName("丈夫");
wife.setName("妻子");
//3.启动线程,开始取钱
husband.start();
wife.start();
}
//方式二:实现Runnable接口
@Test
public void runnable() {
//创建两个线程,分别代表丈夫和妻子
PersonRunnable runnable = new PersonRunnable();
Thread husband = new Thread(runnable);
Thread wife = new Thread(runnable);
husband.setName("丈夫");
wife.setName("妻子");
//启动线程,开始取钱
husband.start();
wife.start();
}
//方式三:实现Callable接口
@Test
public void callable() {
//创建实现Callable接口的实现类的对象
PersonCallable callable1 = new PersonCallable();
PersonCallable callable2 = new PersonCallable();
//创建实现FutureTask类的对象
FutureTask task1 = new FutureTask(callable1);
FutureTask task2 = new FutureTask(callable2);
//创建两个线程,分别代表丈夫和妻子
Thread husband = new Thread(task1);
Thread wife = new Thread(task2);
husband.setName("丈夫");
wife.setName("妻子");
//启动线程,开始取钱
husband.start();
wife.start();
}
}
/**
* 余额为10000账户
*/
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取钱的方法
public void withdraw(double money) {
balance -= money;
}
}
/**
* 取钱的人相当于两个线程
*/
class PersonThread extends Thread {
private Account account;
public PersonThread(Account account) {
this.account = account;
}
@Override
public void run() {
int i = 1;
while (i <= 2) {
synchronized (Account.class) {
account.withdraw(1000);
System.out.println(Thread.currentThread().getName() + "第" + i + "次取钱成功,余额为: " + account.getBalance());
i++;
}
}
}
}
class PersonRunnable implements Runnable {
private Account account = new Account(10000);
@Override
public void run() {
int i = 1;
while (i <= 2) {
synchronized (this) {
account.withdraw(1000);
System.out.println(Thread.currentThread().getName() + "第" + i + "次取钱成功,余额为: " + account.getBalance());
i++;
}
}
}
}
class PersonCallable implements Callable {
private static Account account = new Account(10000);
@Override
public Object call() {
int i = 1;
while (i <= 2) {
synchronized (Account.class) {
account.withdraw(1000);
System.out.println(Thread.currentThread().getName() + "第" + i + "次取钱成功,余额为: " + account.getBalance());
i++;
}
}
return null;
}
}
3.1.2. 同步方法
1、实现规则
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的,在方法名前用synchronized修饰。
2、实例
//只需将3.1.1中实例中的同步代码块去除,然后再withdraw()方法前用synchronized修饰即可,如
public class ThreadSynchronizd {
//方式一:继承Thread类
@Test
public void thread() {
Account acc = new Account(10000);
PersonThread husband = new PersonThread(acc);
PersonThread wife = new PersonThread(acc);
husband.setName("丈夫");
wife.setName("妻子");
husband.start();
wife.start();
}
}
/**
* 余额为10000账户
*/
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取钱的方法
public synchronized void withdraw(double money) {
balance -= money;
}
}
/**
* 取钱的人相当于两个线程
*/
class PersonThread extends Thread {
private Account account;
public PersonThread(Account account) {
this.account = account;
}
@Override
public void run() {
int i = 1;
while (i <= 2) {
account.withdraw(1000);
System.out.println(Thread.currentThread().getName() + "第" + i + "次取钱成功,余额为: " + account.getBalance());
i++;
}
}
}
3. 总结
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是:this; 静态的同步方法,同步监视器是:当前类本身
3.1.3. lock锁——jdk5.0新增
1. 基本步骤
- 实例化ReentrantLock:ReentrantLock lock = new ReentrantLock();
- 调用锁定方法lock():lock.lock();
- 添加需要同步的代码
- 调用解锁方法unlock():lock.unlock();
2. 实例
public class ThreadSynchronizd {
@Test
public void thread() {
Account acc = new Account(10000);
PersonThread husband = new PersonThread(acc);
PersonThread wife = new PersonThread(acc);
husband.setName("丈夫");
wife.setName("妻子");
husband.start();
wife.start();
}
}
/**
* 余额为10000账户
*/
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取钱的方法
public void withdraw(double money) {
balance -= money;
}
}
/**
* 取钱的人相当于两个线程
*/
class PersonThread extends Thread {
private Account account;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
public PersonThread(Account account) {
this.account = account;
}
@Override
public void run() {
int i = 1;
while (i <= 2) {
//2.调用锁定方法lock()
lock.lock();
account.withdraw(1000);
System.out.println(Thread.currentThread().getName() + "第" + i + "次取钱成功,余额为: " + account.getBalance());
i++;
//3.调用解锁方法unlock()
lock.unlock();
}
}
}
3. synchronized 与 Lock的异同?
相同:二者都可以解决线程安全问题
不同:①synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器;
②Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
4. 死锁问题
四、线程通信
1. 实例
//使用两个线程打印 1-100。线程1, 线程2 交替打印
public class PrintNumber {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
class Number implements Runnable {
int i = 0;
@Override
public void run() {
while(true){
synchronized (this) {
notify();
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ": " + i++);
} else {
break;
}
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
2. 涉及到的方法
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
3. 说明
- wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
- wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器;否则,会出现IllegalMonitorStateException异常
- wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
4. sleep() 和 wait()的异同?
- 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
- 不同点: 1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
5. 生产者—消费者模式