进程
概念:正在运行的程序
是系统进行资源分配和调用的独立单位
每一个进程都有它自己的内存空间和系统资源
线程
是进程中的单个顺序控制流,是一条执行路径
单线程:一个进程如果只有一条执行路径,则称为单线程程序
单线程举例:打开记事本,给记事本写内容,当又进行设置纸张大小时,此时再给记事本写内容就写不进去了,这就证明这是一个单线程。
多线程:一个进程如果有多条执行路径,则称为多线程程序
多线程举例:扫雷程序,鼠标控制的游戏点击、页面右上角的时间控制。
Thread类在java.lang包下,使用不需要导包线程是程序中执行的线程,java虚拟机允许应用程序同时执行多个执行线程。
创建一个新的执行线程的方法有两种:
第一种:继承Thread类
①将一个类声明为一个Thread的子类、
public class 子类 extends Thread{}
②该子类应该重写Thread类中的run方法,
@Override
public void run(){}
③创建该子类的对象,
子类 对象名 = new 子类();
④启动线程;
对象名.start();
使用start()方法启动。
void start()导致此线程开始执行,java虚拟机调用此线程的run方法
案例:
public class Demo {
public static void main(String[] args){
MyThread m1 = new MyThread();
MyThread m2 = new MyThread();
// 直接调用MyThread类中的run方法并没有启动线程;
// m1.run();
// m2.run();
// 这种方式 m1执行完0-99的输出之后,m2才开始执行0-99的输出。
// void start()导致此线程开始执行,java虚拟机调用此线程的run方法
m1.start();
m2.start();
// 这种方式,输出结果不再是0-99 0-99 而变成: 0 0-17 1-7 18-99 8-99
}
}
第二种:实现Runnable接口
① 声明一个实现Runnable接口的类
public class 类xxx implements Runnable{}
② 类xxx中重写run方法
@Override
public void run(){
这里要获取线程的名字,不能直接使用getName(),因为本类没有继承Thread类
获取线程名字:Thread.currentThread().getName()
}
③ 创建类xxx的对象
类xxx 对象名x = new 类xxx();
④ 创建Thread类对象,把类xxx对象作为构造方法的参数
Thread 对象名 = new Thread(对象名x);
这样做的好处:
1 没有继承Thread类,好处是可以有它自己的父类 ——避免了java单继承的局限性
2 创建的实现接口Runnable的类,所创建的对象,作为参数传递到Thread构造方法,这样看成一个资源有多个线程。
适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想。
代码:
实现Runnable接口的类
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i = 0 ; i < 100 ; i ++){
System.out.println(Thread.currentThread().getName() + ": " + i);
// 不能直接使用getName方法
}
}
}
测试:
public class Demo {
public static void main(String[] args) {
// 创建类xxx的对象
MyRunnable mr = new MyRunnable();
// 创建Thread类的对象
// 构造方法:Thread (Runnable target)
// 构造方法 Thread (Runnable target,String name)
Thread t1 = new Thread(mr,"pretty");
Thread t2 = new Thread(mr,"sunshine");
// 启动线程
t1.start();
t2.start();
}
}
两个小问题:
为什么重写run()方法?
run()是用来封装被线程执行的代码run方法和start方法的区别?
run():封装线程执行的代码,直接调用,相当于普通方法的调用
start():启动线程;然后由JVM调用此线程的run方法
设置和获取线程名称
| 方法名 | 说明 |
|---|---|
| void setName(String name) | 将此线程的名称更改为等于参数name |
| String getName() | 返回此线程的名称 |
| 构造方法 | 通过构造方法设置线程名称 |
注意:
getName方法是在继承自Thread的子类中使用的
没有设置线程名称的时候,默认的线程名称是 Thread-xsetName方法是在 测试类中书写
对象.setName(String s)实现线程名的设置
要想使用Thread类的带参构造方法,需要在其子类中定义带参构造方法,并使用super(参数);获取main方法的线程的名称
static Thread currentThread() 返回对当前正在执行的线程对象的引用
Thread.currentThread.getName() 获取当前正在执行的线程对象的名称
案例:
测试:
public class Demo {
public static void main(String[] args){
// 无参构造方法给出默认值
// 带参构造方法
System.out.println(Thread.currentThread().getName());
}
}
Thread的子类:
public class MyThread extends Thread{
public MyThread(){}
public MyThread(String name){
super(name);
}
@Override
public void run() {
for(int i = 0 ;i <100 ; i++){
System.out.println(getName() + ": " + i);
}
}
}
线程调度
线程有两种调度模型:
分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片;抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,则随机选择一个。优先级高的线程获取的CPU时间片相对多一些。
java使用的是抢占式调度模型解释:假设只有一个cpu,它在某一个时刻只能执行一条指令,线程只有得到CPU的时间片(使用权)才可以执行指令;多线程程序的执行具有随机性,谁抢到CPU的使用权是不一定的。Thread类中设置和获取线程优先级的方法:
public final int getPriority():返回该线程的优先级
public final void setPriority(int newPriority):更改此线程的优先级
注意:线程默认的优先级是 NORM_PRIORITY = 5,最大的优先级:MAX_PRIORITY = 10,最小的优先级:MIN_PRIORITY = 1线程优先级高仅仅表示线程获取的CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到想要的效果。
代码:
public class Demo {
public static void main(String[] args){
ThreadPriority tp1 = new ThreadPriority();
ThreadPriority tp2 = new ThreadPriority();
ThreadPriority tp3 = new ThreadPriority();
tp1.setName("rose");
tp2.setName("lily");
tp3.setName("sunflower");
// 获取线程的优先级
// 设置参数优先级
// tp1.setPriority(10000);//IllegalArgumentException
// 表示优先级参数不在指定范围内。优先级范围是 MIN_PRIORITY = 1 和 MAX_PRIORITY = 10
// NORM_PRIORITY= 5
tp1.setPriority(1);
tp2.setPriority(5);
tp3.setPriority(10);
tp1.start();
tp2.start();
tp3.start();
}
}
线程控制
| 方法名 | 说明 |
|---|---|
| static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
| void join() | 等待这个线程死亡 也就是当一个线程调用了join方法,则其他线程必须等待这个线程执行完毕之后才能执行 |
| void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,java虚拟机将退出 |
注意:
sleep在继承自Thread类的子类中使用; Thread.sleep()join在测试类中使用setDaemon在测试类中使用
代码:
sleep:
public class ThreadSleep extends Thread {
@Override
public void run() {
for(int i = 0 ;i < 100 ; i++){
System.out.println(getName() + ": " + i);
// 使用sleep方法 使每次执行之后的线程休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
join:
public class ThreadJoinDemo {
public static void main(String[] args) {
ThreadJoin tj1 = new ThreadJoin();
ThreadJoin tj2 = new ThreadJoin();
ThreadJoin tj3 = new ThreadJoin();
tj1.setName("康熙");
tj2.setName("四阿哥");
tj3.setName("八阿哥");
tj1.start();
try {
tj1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
tj2.start();
tj3.start();
}
}
Daemon:
public class ThreadDaemonDemo {
public static void main(String[] args) {
ThreadDaemon td1 = new ThreadDaemon();
ThreadDaemon td2 = new ThreadDaemon();
td1.setName("关羽");
td2.setName("张飞");
// 设置主线程为刘备
Thread.currentThread().setName("刘备");
// 设置守护进程
td1.setDaemon(true);
td2.setDaemon(true);
td1.start();
td2.start();
for(int i = 0; i < 10 ; i++){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
// 案例背景:由于关羽、张飞、刘备桃园三结义,刘备为主线程,所以当刘备执行结束时,关羽张飞也应该停止。
// 也就是设置为守护进程,当主线程停止时,其余设置为守护进程的不是立即消失,而是等一小段时间再消失。
}
}
线程生命周期
包括五个部分:
一:新建——创建线程对象
二:就绪——有执行资格,没有执行权
三:运行——有执行资格,有执行权
四:阻塞——没有执行资格,没有执行权
五:死亡——线程死亡,变成垃圾
需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口买票,请设计一个程序模拟该电影院买票。
类:
public class SellTicket implements Runnable {
private int tickets = 100;
@Override
public void run() {
while(true){
if(tickets > 0){
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
}
}
测试代码:
public class Demo {
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread th1 = new Thread(st,"窗口1");
Thread th2 = new Thread(st,"窗口2");
Thread th3 = new Thread(st,"窗口3");
th1.start();
th2.start();
th3.start();
}
}
案例:卖票改进
需求:每次出票时间100毫秒,用sleep方法实现
代码出现问题:
相同的票出现了多次出现了负数的票
问题的原因:线程执行的随机性导致的。
public class SellTicket implements Runnable {
private int tickets = 100;
// @Override
// public void run() {
// while(true){
// if(tickets > 0){
// // 通过sleep方法来模拟出票时间
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
// tickets--;
// }
// }
// }
// 问题分析:
// 采用sleep模拟出票时间的时候,出现问题
// 问题1 : 相同的票出现多次
// 视频中相同票出现很多次的情况,在自己电脑上运行 出现的是 窗口1售卖所有的票。
// 证实是由于jdk版本问题导致的问题。
// @Override
// public void run() {
// while(true){
// // tickets = 100
// // t1 t2 t3
// // 假设t1线程抢到CPU的执行权
// if(tickets > 0){
// // 通过sleep方法来模拟出票时间
// try {
// Thread.sleep(100);
// // t1 需要等待100毫秒
// // t2 抢到了CPU的执行权,t2开始执行,执行到这里,t2也会等待100毫秒
// // t3 抢到了CPU的执行权,t3开始执行,执行到这里,t3也会等待100毫秒
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// // 假设 按照抢到CPU的执行权的顺序 解释等待的100毫秒
// // t1 抢到CPU的执行权,在控制台输出,窗口1正在出售第100张票
// // t2 抢到CPU的执行权,在控制台输出,窗口1正在出售第100张票
// // t3 抢到CPU的执行权,在控制台输出,窗口1正在出售第100张票
// System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
// // 按理说这里应该是 和 上一代码一起执行的,但是由于三个线程同时运行,这里也会被依次执行三次,最终票变成了97
// tickets--;
// }
// }
// }
// 问题2 : 输出结果售卖的票 变成负数
@Override
public void run() {
while(true){
// tickets = 1
// t1 t2 t3
// 假设t1线程抢到CPU的执行权
if(tickets > 0){
// 通过sleep方法来模拟出票时间
try {
Thread.sleep(100);
// t1 需要等待100毫秒
// t2 抢到了CPU的执行权,t2开始执行,执行到这里,t2也会等待100毫秒
// t3 抢到了CPU的执行权,t3开始执行,执行到这里,t3也会等待100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 假设 按照抢到CPU的执行权的顺序 解释等待的100毫秒
// t1 抢到CPU的执行权,在控制台输出,窗口1正在出售第1张票
// 假设t1抢到了CPU的执行权,就会执行tickets--,这是tickets = 0;
// t2 抢到CPU的执行权,在控制台输出,窗口1正在出售第0张票
// 假设t2抢到了CPU的执行权,就会执行tickets--,这是tickets = -1;
// t3 抢到CPU的执行权,在控制台输出,窗口1正在出售第-1张票
// 假设t1抢到了CPU的执行权,就会执行tickets--,这是tickets = -2;
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
}
}
案例:卖票数据安全问题的解决
判断多线程程序是否会有数据安全问题的标准
是否 是多线程环境是否 有共享数据是否 有多条语句操作共享数据
解决多线程安全问题:基本思想:让程序没有安全问题的环境。
实现:把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。Java提供了同步代码块的方式来解决
代码:
售票类:
public class SellTicket implements Runnable {
private int tickets = 100;
// 要想实现锁住程序,必须使用同一把锁,也就是同一个Object对象
private Object obj = new Object();
@Override
public void run() {
while (true) {
// 使用new Object()这样当每一个线程进来的时候,都会创建一个新的锁,这样是锁不住的。
// tickets = 100
// 线程t1 t2 t3
// 假设t1抢到了cpu执行权
// 假设t2抢到了cpu执行权 继续向下执行 发现被锁了 只能等待
synchronized (obj) {
// t1 进来,把这段代码锁起来了
if (tickets > 0) {
// 通过sleep方法来模拟出票时间
try {
Thread.sleep(100);
// t1休息100毫秒 此时假设t2抢到了cpu执行权
} catch (InterruptedException e) {
e.printStackTrace();
}
// 窗口1正在出售第100张票
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--; // tickets = 99
}else{
break;
}
// t1出来,锁被释放。
}
}
}
}
测试类:
public class Demo {
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread th1 = new Thread(st,"窗口1");
Thread th2 = new Thread(st,"窗口2");
Thread th3 = new Thread(st,"窗口3");
th1.start();
th2.start();
th3.start();
}
}
同步代码块
锁多条语句操作共享数据,可以使用同步代码块实现
格式:
synchronized(任意对象){
多条语句操作共享数据的代码。
}
synchronized(任意对象):将相当于给代码加锁了,任意对象就可以看成是一把锁
同步的好处和弊端:
好处:解决了多线程的数据安全问题
弊端:当线程很多时,每个线程都会去判断同步上的锁,会造成资源的浪费,会降低程序的运行效率。
同步方法:就是把synchronized关键字加到方法上
格式:
修饰符 synchronized 返回值类型 方法名(方法参数){}
同步方法的锁是:this
同步静态方法:就是把synchronized关键字加到static方法上
格式:
public static synchronized 修饰符 方法名(方法参数){}
同步静态方法的锁对象是:类名.class
代码:测试过程中和视频讲解不一样。
public class SellTicket implements Runnable {
// private int tickets = 100;
// 将sellTicket方法设置为static,那么其中的变量tickets也得是static修饰
private static int tickets = 100;
// 要想实现锁住程序,必须使用同一把锁,也就是同一个Object对象
private Object obj = new Object();
private int x = 0;
@Override
public void run() {
while (true) {
if (x % 2 == 0) {
// 原始的是: synchronized (obj) {
// 使用方法sellTicket出现错误,这里改为this就可以了
// 视频中是这样的,但是自己电脑没有出现问题。
// synchronized (this) {
// 使用静态方法 视频出错,
// 这里改为 synchronized (方法名.class) {
// 这种形式 在自己电脑上报错
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
} else {
break;
}
}
} else {
sellTicket();
if (tickets == 0) {
break;
}
}
x++;
}
}
// 视频中,一个采用方法,一个直接书写,会出现两个窗口同时售卖一张篇的情况,原因是 if 和 else 的锁不一样,方法的锁是this,但是现在却没有问题,可能的原因是:jdk视频是1.9 jdk自己是1.8
// 同步静态方法
// 视频中使用静态方法 没有加锁 会报错,但是实际操作中却没有报错
// 视频中给静态方法加synchronized 但是还是报错
// 需要将if中的锁改为sellTicket.class
private static synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
}
线程安全的类
StringBuffer线程安全,可变的字符序列。jdk5之后被StringBuffer替代,它被设计为使用一个线程。通过应该使用StringBuilder类,因为它支持所有相同的操作,但它更快,因为它不执行同步。Vectorimplements List,说明它是List体系的集合,使其成为java Collections framework的成员;Vector类实现了可扩展的对象数组,说明和ArrayList用法相同。两者的区别:Vector被同步,如果不需要线程安全,建议使用ArrayList替代VectorHashtableimplements Map< K, V >说明它和HashMap差不多,使其成为java collections framework的成员;该类实现了一个哈希表,它将键映射到值。任何非null对象都可以用作键值或值;Hashtable被同步,如果不需要线程安全,建议使用HashMap替代Hashtable;总结:1 上述三个类 其中的方法都加了synchronized关键字,说明它是同步的,也就是线程安全的;2 StingBuffer在多线程环境中被使用,剩余两个Vector和Hashtable被xx替代。
1 Vector可以被这个方法所替代。 Collections.synchronizedList(new ArrayList<>()) 2 Hashtable可以被这个方法所替代 Collections.synchronizedMap(new HashMaplock锁())
Lockinterface Lock 说明它是接口它实现提供了比synchronized方法和语句可以获得的更广泛的锁定操作。它提供了获得锁和释放锁的方法:void lock()获得锁;void unlock() 释放锁它是接口不能直接创建对象,所以需要它的实现类,使用ReentrantLockReentrantLock():创建一个ReentrantLock的实例
理解:
加锁和释放锁的工作是在Runnable的实现类中加的
一般采用如下形式加锁以及释放锁:
private Lock lock = new ReentrantLock();
try{
lock.lock();
}finally{
lock.unlock();
}
代码:
public class SellTicket implements Runnable {
private int ticket = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while(true) {
try {
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Thread.currentThread().getName() 获取当前正在运行的线程的名字
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
ticket--;
}
}finally {
lock.unlock();
}
}
}
}
生产者消费者
生产者消费者模式是一个十分经典的多线程写作的模式生产者消费者问题,包含两个线程:①生产者线程用于生产数据②消费者线程用于消费数据生产者消费者关系解耦 :生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;消费者只需要从共享数据区中获取数据,并不需要关心生产者的行为。Object类的等待和唤醒方法:
| 方法名 | 说明 |
|---|---|
| void wait() | 导致当前线程等待,直到另一个线程调用该对象的notify()方法或者notifyAll()方法 |
| void notify() | 唤醒正在等待对象监视器的单个线程 |
| void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
代码:
// 奶箱类 表示共享数据区
public class Box {
private int milk;
// 定义成员变量,表示奶箱的状态
private boolean state = false;
// 报异常IllegalMonitorStateException 因为 wait方法没有使用在同步中。
// 存储牛奶
public synchronized void put(int milk){
// 如果奶箱有牛奶,等待消费
if(state){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有牛奶,就生产牛奶
this.milk = milk;
System.out.println("送奶工将第" + this.milk + "瓶奶放入奶箱");
// 生产完毕之后,修改奶箱状态
state = true;
// 唤醒其他等待的线程
notifyAll();
}
// 获取牛奶
public synchronized void get(){
// 如果奶箱没有牛奶,就等待
if(!state){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果 有就可以取牛奶
System.out.println("用户拿到第" + this.milk + "瓶奶");
// 这里不应该milk--吗?
// 消费完毕之后,修改奶箱状态
state = false;
// 唤醒其他线程
notifyAll();
}
}
// 生产者类
public class Producer implements Runnable {
private Box b;
public Producer(Box b){
this.b = b;
}
@Override
public void run() {
for(int i=1;i<=5;i++){
b.put(i);
}
}
}
// 消费者类
public class Customer implements Runnable {
private Box b;
public Customer(Box b){
this.b = b;
}
@Override
public void run() {
while(true){
b.get();
}
}
}
// 测试类
public class Demo {
public static void main(String[] args) {
Box b = new Box();
Producer p = new Producer(b);
Customer c = new Customer(b);
Thread t1 = new Thread(p,"生产者");
Thread t2 = new Thread(c,"消费者");
t1.start();
t2.start();
}
}



