一、常用概念
1.程序
Java源程序和字节码文件被称为“程序” ( Program ),是一个静态的概念
2.程序
执行中的程序叫做进程(Process),是一个动态的概念。为了使计算机程序得以运行,计算机需要加载代码,同时也要加载数据。
- 进程是程序的一次动态执行过程, 占用特定的地址空间。
- 每个进程由3部分组成:cpu,data,code。每个进程都是独立的,保有自己的cpu时间,代码和数据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西。
- 多任务(Multitasking)操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每个进程独立运行。以进程的观点来看,它会以为自己独占Cpu的使用权
3.进程
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
程序是指令的集合,代码的集合 ; 而进程是动态的概念,当程序在执行时,系统分配进程 ; 多线程是在同一进程下,充分利用资源 ,多条执行路径,共享资源 (cpu data code) 。
二、创建线程
1.继承Thread
- 创建线程类: 继承 Thread类 +重写 run() 方法
- 构造线程类对象: 创建 子类的对象
- 启动线程: 通过子类对象调用 start() 方法
创建 Thread 子类的一个实例并重写 run 方法, run 方法会在调用 start() 方法之后自动被执行
/*
1、创建一个线程类 继承 Thread + 重写 run方法
2、创建线程对象
3、调用线程对象的 start方法启动线程
*/
public class Demo001 {
public static void main(String[] args) {
//创建多线程对象
MyThread thread=new MyThread();
for (int i=0;i<100;i++){
System.out.println(i);
}
thread.run();
}
}
class MyThread extends Thread{
@Override
public void run() {
super.run();
System.out.println("子线程的方法");
}
}
这种方式的特点:由于java中类只能单继承,因此继承Thread后就不能再继承其他类,由于父类的run方法中没有抛出异常,因此异常只能捕获。
2.实现Runnable接口实现
- 创建实现 Runnable 接口的实现类 + 重写 run() 方法
- 创建一个实现类对象
- 利用实现类对象创建Thread类对象
- 启动线程
/*
通过实现Runnable接口创建线程
1、创建线程类 实现Runnable接口, 重写run方法
2、创建线程类对象 -> 提供真实的线程操作
3、创建Thread类对象,需要持有 线程对象的引用 -> 提供代理方法(启动线程的方法)
4、通过Thread对象启动线程
*/
public class Demo003 {
public static void main(String[] args) {
// 创建线程类对象
MyThreadRunnable t = new MyThreadRunnable();
// t提供线程体, Thread 提供启动
Thread thread = new Thread(t);
thread.start();
for(int i=0; i<200; i++){
System.out.println("。。。。");
}
}
}
class MyThreadRunnable implements Runnable{
// 线程体方法
@Override
public void run() {
System.out.println("---------------------");
}
}
3. 实现Callable接口实现(了解)
- 创建实现 Callable 接口的实现类 + 重写 call() 方法
- 创建一个实现类对象
- 由 Callable 创建一个 FutureTask 对象
- 由 FutureTask 创建一个 Thread 对象
/*
*通过Callable接口实现多线程
* 1、创建线程类 实现Callable接口 + 重写 call方法
* 2、创建线程类对象
* 3、创建FutureTask对象
* 4、创建Thread对象
*/
public class Demo002 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable callable=new MyCallable();
// 将Callable包装成FutureTask,FutureTask也是一种Runnable
FutureTask futureTask=new FutureTask(callable);
// 将FutureTask包装成Thread
new Thread(futureTask).start();
System.out.println(futureTask.isDone());
System.out.println(futureTask.get());
}
}
class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum=0;
for (int i=0;i<100;i++){
sum+=i;
}
return sum;
}
}
4.通过线程池创建
三、线程的五种状态
- 新建状态 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态 当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态 如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O处理完毕,线程重新转入就绪状态。 - 死亡状态 一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
1.停止线程
死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。
- 正常运行的线程完成了它的全部工作
- 线程被强制终止,如通过执行 stop 或 destroy 方法来终止一个线程。但是,不要调用 stop,destory 方法 ,太暴力,一盆冷水让其停止。
/**
* 1、正常执行完毕,循环 次数已经到达
* 2、外部干涉
* 线程中加入标识 -->属性
* 线程体中 使用改标识 -->死循环
* 对外提供改变改标识的方法 setXxx() terminate() a()...
* 外部根据适当的时机调用该方法
*/
public class ThreadStop {
public static void main(String[] args) throws InterruptedException {
MyThread mt=new MyThread();
Thread t=new Thread(mt);
t.start();
Thread.sleep(2000);
mt.setFlag(false);
System.out.println("主线程方法");
}
}
class MyThread extends Thread{
private boolean flag=true;
@Override
public void run() {
int i=0;
while (flag){
System.out.println("我是子线程"+i++);
try {
//暂停当前的线程,时间为100ms
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//设置线程运行状态的方法
public void setFlag(boolean flg){
this.flag=flg;
}
}
2.阻塞状态
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。
有三种方法可以让我们暂停Thread执行:
- sleep方法:sleep() 方法可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态。但是sleep() 方法不会释放“锁标志”,也就是说如果有 synchronized 同步块,其他线程仍然不能访问共享数据。
- yield方法: yield() 方法和 sleep() 方法类似,也不会释放“锁标志”,区别在于yield()方法是“礼让”一次,即yield() 方法让当前线程在被调用时进入就绪状态,系统分配资源时会参与线程的竞争
- join方法: 方法会使当前线程等待调用 join() 方法的线程结束后才能继续执行
[1]sleep()
public class PauseThread {
public static void main(String[] args) {
MyThread02Runnable m2=new MyThread02Runnable();
MyThread03Runnable m3=new MyThread03Runnable();
new Thread(m2).start();
new Thread(m3).start();
}
}
class MyThread02Runnable implements Runnable{
@Override
public void run() {
for(int i=0; i<10; i++){
System.out.println("你打我一下");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread03Runnable implements Runnable{
@Override
public void run() {
for(int i=0; i<10; i++){
System.out.println("我打你一下");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
[2]yield()
public class PauseThread2 {
public static void main(String[] args) {
MyThread04Runnable m4=new MyThread04Runnable();
MyThread05Runnable m5=new MyThread05Runnable();
new Thread(m4).start();
new Thread(m5).start();
}
}
class MyThread04Runnable implements Runnable{
@Override
public void run() {
for(int i=0; i<10; i++){
System.out.println("你打我一下");
}
}
}
class MyThread05Runnable implements Runnable{
@Override
public void run() {
for(int i=0; i<10; i++){
//当被调用时,礼让以下。进入到就绪状态,系统分配资源时会参与线程的竞争
Thread.yield();
System.out.println("我打你一下");
}
}
}
[3]join()
public class PauseThread3 {
public static void main(String[] args) throws InterruptedException {
MyThread06Runnable m6 = new MyThread06Runnable();
Thread t=new Thread(m6);
t.start();
// 当主线程 输出前5条语句,前 5条我们都是公平,但是到5条以后,我希望 我自己的线程
// 加入到主线程,要主线程等待我的线程执行结束后再执行
for (int i=0;i<10;i++){
System.out.println("main========");
if (i==3){
//谁调用加入到谁的线程中,执行完毕后会继续执行原线程
t.join();
}
}
}
}
class MyThread06Runnable implements Runnable{
@Override
public void run() {
for(int i=0; i<10; i++){
System.out.println("我打你一下");
}
}
}
3.线程的基本信息
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照 线程的优先级 决定应调度哪个线程来执行。
线程的优先级用数字表示,范围从 1 到 10
一线程的默认优先级是5
Thread.MIN_PRIORITY = 1
Thread.MAX_PRIORITY = 10
Thread.NORM_PRIORITY = 5
使用下述线程方法获得或设置线程对象的优先级
void setPriority(int newPriority);
void setPriority(int newPriority);
注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高后调用优先级低的线程
public class ThreadTest4 {
public static void main(String[] args) {
Thread t1 = new Thread(new Thread7(), "t1");
Thread t2 = new Thread(new Thread7(), "t2");
t1.setPriority(1);
t2.setPriority(10);
t1.start();
t2.start();
}
}
class Thread7 extends Thread {
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i); // yield();
}
}
}
四、线程同步
1. 线程安全
在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就有可能造成数据的不准确。
public class TestSync {
public static void main(String[] args) {
Account a=new Account(100,"a");
Drawing dra1=new Drawing(60,a);//你取钱
Drawing dra2=new Drawing(70,a);//你家人取钱
dra1.start();
dra2.start();
}
}
/**
* 简单表示银行账户
*/
class Account{
int monery;
String name;
public Account(int monery, String name) {
this.monery = monery;
this.name = name;
}
}
/**
* 简单表示取钱操作
*/
class Drawing extends Thread{
int drawNum; //要取的钱数
Account account;//要取钱的账户
int expenseTotal;//总共取的钱数
public Drawing(int drawNum, Account account) {
super();
this.drawNum = drawNum;
this.account = account;
}
@Override
public void run() {
draw();
}
void draw() {
if (account.monery - drawNum < 0) {
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.monery = account.monery - drawNum;
expenseTotal = expenseTotal + drawNum;
System.out.println(this.getName() + " 共取了" + expenseTotal);
System.out.println(this.getName() + " 账户余额" + account.monery);
}
}
执行结果为两人都在账户中取到钱了,而且账户余额还剩40,但是我们代码定义的逻辑是余额不足则取不出钱。很显然,使用多线程但未处理共用资源时,线程是不安全的。
2. 线程同步 synchronized
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。“同”字从字面上容易理解为一起动作其实不是,“同”字应是指协同、协助、互相配合。
在Java里面,通过 synchronized 进行同步的保证。它包括两种用法:synchronized 方法和 synchronized 块
[1]synchronized 方法
通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:
public synchronized void accessVal(int newVal);
synchronized 方法控制对类成员变量的访问:每个对象对应一把锁,每个synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得 该锁,重新进入可执行状态。
synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。
[2]synchronized 块
public class TestSync {
public static void main(String[] args) {
//创建账户对象
Account a=new Account(100,"a");
//创建两个线程对象
Drawing dra1=new Drawing(60,a);//你取钱
Drawing dra2=new Drawing(50,a);//你家人取钱
dra1.start();
dra2.start();
}
}
/**
* 简单表示银行账户
*/
class Account{
int monery;
String name;
public Account(int monery, String name) {
this.monery = monery;
this.name = name;
}
}
/**
* 简单表示取钱操作
*/
class Drawing extends Thread{
int drawNum; //要取的钱数
Account account;//要取钱的账户
int expenseTotal;//总共取的钱数
public Drawing(int drawNum, Account account) {
super();
this.drawNum = drawNum;
this.account = account;
}
@Override
public void run() {
draw();
}
//取钱的方法
void draw() {
//如果账户余额小于要取的钱数,则结束方法 (双重检测来提高效率)
if (account.monery - drawNum < 0) {
return;
}
//锁住账户对象
synchronized (account) {
//若余额比将取出的钱少则返回(逻辑判断)
if (account.monery - drawNum < 0) {
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//取钱,账户扣款
account.monery = account.monery - drawNum;
//取出的钱数
expenseTotal = expenseTotal + drawNum;
System.out.println(this.getName() + " 共取了" + expenseTotal);
System.out.println(this.getName() + " 账户余额" + account.monery);
}
}
}
使用了synchronized块后,第一个人取完钱后余额还剩40,第二个人取50发现钱不够,就不再进行取款。此时线程变得安全了。
上面这种方式叫做:互斥锁原理。利用互斥锁解决临界资源问题。
五、死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
下面举个小例子
如:A去B公司面试:
A:你把死锁说清楚我就让你通过面试
B: 你让我过通过面试我就把死锁给你说清楚
/**
* 测试死锁
*/
public class ThreadDeadLock {
public static void main(String[] args) {
Makeup m1=new Makeup(true,"张三");
Makeup m2=new Makeup(false,"李四");
m1.start();
m2.start();
}
}
/**
* 口红
*/
class Lipstick{
}
/**
* 镜子
*/
class Mirror{
}
class Makeup extends Thread{
//确定获取资源的顺序
boolean flag;
//名称
String girl;
// 静态属性是所有对象共享
static Lipstick lipstick=new Lipstick();
static Mirror mirror= new Mirror();
// 创建对象的时候, 就确定先获取什么资源
public Makeup(boolean flag,String girl) {
this.flag = flag;
this.girl = girl;
}
@Override
public void run() {
doMakeUp();
}
void doMakeUp(){
if (flag){
synchronized (lipstick){
System.out.println(girl+" 拿着口红");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (mirror){
System.out.println(girl+ "拿着镜子");
}
}
}else {
synchronized (mirror){
System.out.println(girl+" 拿着镜子");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lipstick){
System.out.println(girl+"拿着口红");
}
}
}
}
}
如果不强行停止程序,就会一直处于阻塞状态。张三一直拿着镜子,但是需要拿口红才能完成化妆,但是口红在李四那,而李四需要拿着镜子才能化妆。他们谁都不相让,因此程序一直阻塞。
如何解决死锁问题:
- 往往是程序逻辑的问题。需要修改程序逻辑。
- 尽量不要同时持有两个对象锁。
六、生产者/消费者模式
在常见的多线程问题解决中,同步问题的典型示例是“生产者-消费者”模型,也就是生产者线程只负责生产,消费者线程只负责消费,在消费者发现无内容可消费时则调用则唤醒生产者,自身则调用wait()进入阻塞状态。
而生产者发现生产量到达一定程度之后,则唤醒消费者,自身进入休眠状态。
/**
* 生产者,消费者
*/
public class Synchronized {
public static void main(String[] args) {
//有生产、消费功能的工厂
SyncStack stack=new SyncStack();
//生产馒头的工厂类
ShengChan sc=new ShengChan(stack);
//有消费功能的工厂
Chi chi=new Chi(stack);
sc.start();
chi.start();
}
}
/**
* 商品类
*/
class Mantou{
}
/**
* 有生产、消费功能的工厂
*/
class SyncStack{
List<Mantou> list=new ArrayList<>();//盛馒头的容器。10个就满了
//生产馒头的方法
public synchronized void push(Mantou mantou){
//若list已经装了10个元素,则认为已经放满了
if (list.size()==10){
try {
//唤醒消费者的线程,通知来买馒头
this.notify();
//自身进入阻塞状态,等待被唤醒
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
list.add(mantou);
System.out.println("生产第"+list.size()+"个馒头");
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 吃馒头的方法
*/
public synchronized void pop(){
//如果吃完了
if (list.size()==0){
try {
//唤醒生产者
this.notify();
//自身进入睡眠
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//如果还有馒头,则移除list中最上面的元素
list.remove(list.size()-1);
System.out.println("还剩"+list.size()+"个馒头");
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//生产馒头的线程
class ShengChan extends Thread{
SyncStack stack;
public ShengChan(SyncStack stack) {
this.stack = stack;
}
@Override
public void run() {
while (true){
//一直生产馒头
Mantou mantou=new Mantou();
this.stack.push(mantou);
}
}
}
//消费馒头的线程
class Chi extends Thread{
SyncStack stack;
public Chi(SyncStack stack) {
this.stack = stack;
}
@Override
public void run() {
while (true){
//一直吃馒头
this.stack.pop();
}
}
}