并发编程基础——线程
线程
1. 程序、进程、线程
思考什么是程序?什么是进程?什么是线程
- 程序(prigram):程序是指令和数据的有序集合,也就是我们写的代码,是静态的,
- 进程:进程是程序执行的过程,是系统分配资源的最小单位
- 线程:进程中存在最少一个线程(主线程),CPU为进程分配资源,调度线程来使用这些资源,线程是CPU调度的最小单元,程序真正在执行的是线程
Java中的main方法实际上也是一个线程,我们将代码写好,交给这个main线程去处理内存问题,就和我们在写线程的run方法一样
Java内存模型
2. 创建线程的方式
-
继承Thread类
- 由于单继承的局限性,不建议使用
-
实现Runnable接口
- 避免了单继承的局限性,灵活方便,方便同一个对象被多个线程使用
-
使用lambda表达式简化操作
public class CreateThreadTest1 {
//第一种创建方式,声明一个Thread子类,重写run方法
//第二种方式,带参数构造器传入实现了Runnable接口的对象
//star()方法将将线程交给CPU调度
//在A线程中创建B线程,二者优先级相同,会竞争CPU资源
public static void main(String[] args) {//主线程
Thread thread1 = new MyThread();
Thread thread2 = new Thread(new MyRunnable());
Thread thread3 = new Thread(()->{
for (int i = 0; i < 30; i++) {
System.out.println("豆豆不哭" + i);
}
});
thread1.start();
thread2.start();
thread3.start();
//主线程中的方法
for (int i = 0; i < 3; i++) {
System.out.println("吃饭"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("睡觉"+i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("打豆豆"+i);
}
}
}
//测试结果
/*
吃饭0
吃饭1
吃饭2
豆豆不哭0
打豆豆0
睡觉0
打豆豆1
豆豆不哭1
打豆豆2
睡觉1
睡觉2
豆豆不哭2
*/
- 龟兔赛跑案例练习
public class TorAndHero {
public static void main(String[] args) {
Run run = new Run(100);
Thread thread1 = new Thread(run,"兔子");
Thread thread2 = new Thread(run,"乌龟");
thread1.start();
thread2.start();
}
}
class Run implements Runnable{
String win = null;//胜者
int length;//跑道长度
public Run(int length) {
this.length = length;
}
@Override
public void run() {
for (int i = 1; i <= length; i++){
isSleep(i);//兔子中途睡5s,每步需要0.2s,乌龟每步需要0.5s不休息
if (isGameOver(i))break;//如果win不是空,打印胜利者,循环退出
System.out.println(Thread.currentThread().getName()+i);
}
}
public boolean isGameOver(int step) {
if (win!=null) {
return true;
}
if (step>=length){
win=Thread.currentThread().getName();
System.out.println("胜利者是" + win);
return true;
}
return false;
}
public void isSleep(int step){
if (Thread.currentThread().getName()=="兔子"){
if (step==length/2)
sleep(5000);
else sleep(200);
}if (Thread.currentThread().getName()=="乌龟"){
sleep(500);
}
}
public void sleep(long time){
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3. 并发问题
- 多个线程同时操作一个对象会发生数据紊乱问题
// 并发问题模拟
public class ConcurrentProblem {
public static void main(String[] args) {
BuyRunnable buyRunnable = new BuyRunnable(5);
new Thread(buyRunnable, "关羽").start();
new Thread(buyRunnable, "张飞").start();
new Thread(buyRunnable, "赵云").start();
new Thread(buyRunnable, "马超").start();
new Thread(buyRunnable, "黄忠").start();
}
}
class BuyRunnable implements Runnable {
private int tickNum;//票数
public BuyRunnable(int tickNum) {
this.tickNum = tickNum;
}
@Override
public void run() {
while (tickNum > 0) {//不能用!=判断,在多线程的情况下会出现死循环!
System.out.println(Thread.currentThread().getName() + "取得了第" + tickNum-- + "张票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//测试结果(不一样)
/*
关羽取得了第5张票
赵云取得了第3张票
张飞取得了第4张票
马超取得了第2张票
黄忠取得了第1张票
赵云取得了第0张票
张飞取得了第-1张票
关羽取得了第-1张票
Process finished with exit code 0
*/
- 产生的原因
Java中都是值传递,系统会给线程分配一块内存,在线程中变量更新不是原子操作,而是分步骤的
从内存中将数据备份到线程私有内存区—>修改备份—>然后用备份覆盖内存中的数据
重复写:多个线程可以同时获得一个数据的备份,都对备份数据执行-1操作 回写相同的结果
跳过判断:如果线程A获得备份时ticktNum是1,在覆盖前b进入循环然后进入睡眠状态,A写回数据成功,而这时b线程已经跳过判断步骤,苏醒后备份拿到的ticktNum是0这个不合法的值,还对这个值进行了--,同理,当线程C苏醒后再获得这个负值(增加线程停止判断)
跳过取:线程独立运行,完成的速度也不相同,资源共享情况下,线程a在修改完数据还没有打印,线程b再获得这个修改后的数据提前完成打印,产生跳过取得线程
总结,多个线程争抢同一个数据会导致数据紊乱,出现各种奇葩现象,这就是线程不安全
思考:为什么龟兔赛跑没有出现这种情况?获取length只是用来比较的,值没有修改和写回的步骤
- 如何解决?
往下看。。。
5. 线程状态
- 创建状态 线程被创建new
- 就绪状态 调用start()方法,等待cpu调度进入运行状态,不调度进入阻塞状态
- 阻塞状态 调用sleep、wait或同步锁,线程进入阻塞状态
- 运行状态 cpu调度后执行线程中的run方法代码块
- 死亡状态 线程结束,不能再次启动
6. 线程控制
-
利用标志位停止线程
-
线程休眠
-
线程礼让
-
线程强制执行
-
获取线程当前状态
-
线程优先级设置
-
守护线程
7. 线程同步
-
多个线程操作同一个对象,也叫线程并发
-
并发会产生数据紊乱问题,这个问题该如何解决呢?——队列+锁
-
当一个线程需要获取这个资源的时候,需要先获得这个资源的锁,如果无法获得这个资源的锁,那么就进入等待队列(挂起)
-
线程锁机制synchronized
//两种格式
//1.放在方法名前,在调用方法前获取调用这个方法的this对象锁,也就是this;
//2.在方法中定义并发代码块,指定要获得哪个对象锁(微操)
{
synchronized(){
}
}
- 练习案例
//买票案例
public class UnsafeBuy {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket(100);
Thread thread1 = new Thread(buyTicket,"线程1");
Thread thread = new Thread(buyTicket,"线程2");
Thread thread3 = new Thread(buyTicket,"线程3");
Thread thread4 = new Thread(buyTicket,"线程4");
thread.start();
thread1.start();
thread3.start();
thread4.start();
}
}
class BuyTicket implements Runnable {
private int ticket;
private boolean flag = true;
public BuyTicket(int ticket) {
this.ticket = ticket;
}
@Override
public void run() {
while (flag) {
buy();
}
}
private synchronized void buy() {//买票时会改变票数,在此加锁
if (ticket >= 0) {
System.out.println(Thread.currentThread().getName() + "取得了" + ticket--);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
flag = false;
}
}
}
//银行取钱案例
public class UnSafeBank {
public static void main(String[] args) {
Account account = new Account(110, 100);
new Bank(account, 10).start();
new Bank(account, 20).start();
}
}
class Bank extends Thread {
int needMoney;
Account account;
boolean flag = true;
public Bank(Account account, int needMoney) {
this.account = account;//这里不是拷贝这个对象,而是将自己的account指向了这个对象,所以两个线程使用的其实是一个对象
this.needMoney = needMoney;
}
//取钱操作,改变的是账户,给账户加锁
private void withdrawMoney() {
synchronized (account){
System.out.println(account.hashCode());//可以测试一下,两个线程操作的是一个对象哦
if (account.money >= needMoney) {
account.money -= needMoney;
System.out.println(Thread.currentThread().getName() + "--" + needMoney + "——余额" + account.money);
} else {
System.out.println(Thread.currentThread().getName() + "need" + needMoney + "余额不足");
flag = false;
}
}
}
//测试用的sleep
public void mySleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
if (flag) {
withdrawMoney();
}
}
}
class Account {
int id;
int money;
public Account(int id, int money) {
this.id = id;
this.money = money;
}
}
//集合案例
public class UnSafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i=1; i<=10000; i++) {
new Thread(() -> {
synchronized (list) {//可能多个线程操作在同一区域写操作产生覆盖,在这一行代码给list加锁
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(100);
System.out.println(list.size());
}
}
//JUC中定义了一个线程安全的集合类CopyOnWriteArrayList
总结:
- synchronized意思是尝试获取括号中的对象的锁,如果获得这个对象的锁,那么执行并发块中代码,并发块执行完毕后释放锁,因为一个对象只有唯一的一个锁,如果别的线程需要执行这段代码,则必须等待这个锁被释放才有可能获得;
- 当线程在执行到某块代码时会改变共享对象,就不安全了,需要考虑把这段代码放入并发代码块,用synchronized获得这个对象锁,让线程排队,避免数据紊乱
- synchronized锁机制的缺陷
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争的情况下,会产生延时问题
- 如果一个优先级高的线程等待优先级低的线程,会导致优先级倒置
8. 死锁
什么是死锁?
死锁就是两个以上线程互相请求对方已经占有的资源,导致都无法进行下去的一种现象
死锁产生的四个必要条件
- 互斥 :对于线程共享的资源,同一时刻只能有一个线程可以使用
- 请求保持:线程请求不到资源不会停止
- 不可剥夺:线程不会释放已经获得的资源
- 形成环路:多个线程之间资源请求形成环路
即有资源a资源b线程1线程2,线程1,2都需要资源a和b,线程1获得a等待b,线程2获得b等待a,但二者都不会释放已经拥有的资源,那么两个线程一直等待资源却不会有结果,就都不可能继续执行下去,形成死锁;
- 模拟死锁代码
//模拟死锁
public class DeadLock {
public static void main(String[] args) {
Mirror mirror = new Mirror();
Comb comb = new Comb();
Runnable makeUp = new Makeup(mirror,comb);
new Thread(makeUp).start();
new Thread(makeUp).start();
}
}
class Makeup implements Runnable {
Mirror mirror;
Comb comb;
//标记位,如果获取不到镜子就获取梳子
boolean flag = true;
public Makeup(Mirror mirror, Comb comb) {
this.mirror = mirror;
this.comb = comb;
}
private void doMake() {
if (flag) {//有镜子
synchronized (comb) {
flag = false;
comb.getComb();
mySleep(100);
synchronized (mirror) {
mirror.getMirror();
}
}
} else {
synchronized (mirror) {
mirror.getMirror();
mySleep(100);
synchronized (comb) {
flag = false;
comb.getComb();
}
}
}
flag = true;
}
private void mySleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
doMake();
}
}
//资源类
class Mirror {
public void getMirror() {
System.out.println(Thread.currentThread().getName() + "获得镜子");
}
}
class Comb {
public void getComb() {
System.out.println(Thread.currentThread().getName() + "获得梳子");
}
}
- 怎么解决?不要贪心,一次只拿一个资源的锁,不要再同步块中再拿别的资源的锁
synchronized (comb) {
flag = false;
comb.getComb();
mySleep(100);
}
synchronized (mirror) {
mirror.getMirror();
}
9. Lock锁
- lock锁对代码块加锁,加锁和解锁这段区域的代码线程在调用时排队
- lock锁锁的是lock自己,synchrized锁的是对象
- lock锁有很多子类,具有更好的扩展性,使用lock锁JVM调度线程花费的时间较少
- 优先使用顺序:lock锁>同步代码块(在方法内部具体代码加锁,资源已经分配)>同步方法(在方法外加锁,资源还没有分配)
- 使用格式
try{
locke.lock();
//...并发代码
}finally{
lock.unlock();//代码执行完后必须释放锁
}
问题: 为什么要这样写,为什么在下面的代码里不这样写程序不会结束?
答: finally{}保证了锁一定会被释放,在下面的案例中如果没有finally{},当线程1如果取到了最后一张票时,直接跳出循环,不会执行释放锁的操作,线程2一直停留在lock()请求锁这一步中,不会死亡,也不会执行下面的语句,所以整个程序不会停止;
问题: 如果在while外面lock()会怎么样?
答:那就只有一个线程可以买票了,票买光了第二个线程才能进去,虽然线程安全了,但是程序只有一个线程可用,失去了多线程的意义
lock锁解决买票问题
public class TestLock {
public static void main(String[] args) {
LockRun lockRun = new LockRun();
new Thread(lockRun).start();
new Thread(lockRun).start();
}
}
class LockRun implements Runnable {
private int tickets = 5;
Lock lock = new ReentrantLock();//定义一个锁
@Override
public void run() {
while (true) {
try{
lock.lock();//获得锁
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + tickets--);
} else{
break;
}
}finally {
lock.unlock();//释放锁
}
}
}
}
10. 线程协作
生产者消费者问题
生活中的案例:一个人麦当劳买炸鸡,前台如果有炸鸡就拿走,没有炸鸡要等厨师做好放在前台;
厨师做炸鸡,前台没有炸鸡要做炸鸡,前台有了炸鸡就等人买过了再做炸鸡
有三个对象:生产者(厨师)【只管生产】
消费者(买炸鸡的人)【只管消费】
缓冲区(前台)【存储资源,唤醒线程,让线程等待】
线程通信问题:生产者是个线程,消费者是个线程,缓冲区是它们共享的一个区域,用来存储资源,消费者消耗缓冲区资源后缓冲区唤醒生产者,生产者生产资源放入缓冲区唤醒消费者,当缓冲区满时让生产者等待消费者,当缓存区空时让消费者等待生产者。
线程通信方法:wait()线程等待
notify()唤醒一个等待状态的线程
notifyAll()唤醒同一个对象上所有等待状态的线程,cpu按照线程优先级调度
这些都是Object类的方法,只能放在同步方法或同步代码块中,否则会会抛出异常lllegalMonitorStateException//非法监视器状态异常
线程通信机制就是等待和唤醒机制;
- 代码演示
管程法:把资源和操作资源的方法封装在一起,通过一些判断来控制线程等待还是执行
标志位法:通过信号量来判断资源是否存在,使线程交替执行,不暂存资源
缓冲区法:将资源放在一个缓冲区中,通过缓冲区状态来判断哪个线程该执行
注意事项:一个管程只管理一对生产者和消费者,因为wait会释放锁,唤醒可能会跳过获取锁的步骤
//通过缓冲区解决生产者消费者问题——管程法(一个缓冲区管理一对生产者,消费者线程)
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer(2);//创建一个缓冲区
SynContainer container1 = new SynContainer(4);//创建第二个缓冲区
new Thread(new Producer(container1)).start();//第二个缓冲区的生产者
new Thread(new Consumer(container1)).start();//第二个缓冲区的消费者
Producer producer = new Producer(container);//创建一个生产者
Consumer consumer = new Consumer(container);//创建一个消费者
Thread proThread = new Thread(producer);//创建生产线程
Thread conThread = new Thread(consumer);//创建消费线程
proThread.start();//开启线程
conThread.start();
}
}
//生产者只负责生产
class Producer implements Runnable {
//添加一个缓冲区
private SynContainer synContainer;
public Producer(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
//规定生产者生产完五只鸡结束
for (int i = 1; i <= 5; i++) {//生产者生产的数量要和消费者消费的数量一致,否则会有有一方因为等待资源而发生死锁
Chicken chicken = new Chicken(i);//生产鸡
synContainer.push(chicken); //把鸡放进缓冲区
System.out.println("生产了第" + i + "只鸡");
}
}
}
//消费者只负责消费
class Consumer implements Runnable {
//添加一个缓冲区
private SynContainer synContainer;
public Consumer(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {//规定消费者消费完五只鸡结束
System.out.println("消费了第" + synContainer.pop() + "只鸡");
}
}
}
//缓冲区
class SynContainer {
private int capacity;
private Chicken[] chickens;
int count = 0;//计数器
public SynContainer(int capacity) {
this.capacity = capacity;
this.chickens = new Chicken[capacity];//不要在上面直接写private Chicken[] chickens = new Chicken[capacity]
//因为全局int变量是初值是0,相当于new Chicken[0],在变量声明的时候就会给它分配空间,应该在构造器中初始化它
//当然也可以两个地方都写,但是这样的话在new的时候chickens变量指向构造函数里创建的数组区域,JVM会进行垃圾回收,把全局中创建的数组清理掉
//所以何必要让垃圾处理器多处理一次呢?
}
//缓冲区被放入产品
public synchronized void push(Chicken chicken) {
if (count == chickens.length-1) {
try {
//如果在同一个缓冲区创建了多个生产者和消费者依然是不安全的,
// 因为wait让线程停止在wait()这里,同时会释放锁,如果在这时另一个生产者获得了这个锁也停在了这里,当消费者执行notifyAll()后
//两个线程都会被唤醒且已经跳过了获取锁的步骤,争抢资源,同理,开启多个消费者线程也会这样
// 解决办法是什么呢?一个缓冲区只能被一对消费者和生产者操作,不要被多个生产、消费者共享,创建另外一个新的缓冲区
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
chickens[count] = chicken;
count++;
this.notifyAll();
}
//缓冲区取出产品
public synchronized Chicken pop() {
if (count == 0) {
try {
this.wait();//停在了这里,被唤醒之后也是从这里执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Chicken chicken = chickens[count-1];//消费者拿到一只鸡
count--;
this.notifyAll();
return chicken;
}
}
//资源类
class Chicken {
private int number;
public Chicken(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
}
标志位法;
//信号量法解决消费者生产者问题
public class SignalPC {
public static void main(String[] args) {
//看厨师是否做好了包子,做好了再吃,吃完了再做,做完再吃。。。
BaoZiPu baoZiPu = new BaoZiPu();
new Cook(baoZiPu).start();
new Foodie(baoZiPu).start();
}
}
//厨师
class Cook extends Thread {
private BaoZiPu baoZiPu;
public Cook(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {//做五个包子
baoZiPu.doBaoZi();
}
}
}
//吃货
class Foodie extends Thread{
private BaoZiPu baoZiPu;
public Foodie(BaoZiPu baoZiPu) {
this.baoZiPu = baoZiPu;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
baoZiPu.eat();
}
}
}
//包子铺(管程)
class BaoZiPu {
BaoZi baoZi;
boolean flag;//厨师做好包子为ture,吃货吃完包子为false
//吃包子
public synchronized void eat(){
if (!flag){//没包子时等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
baoZi = null;//垃圾处理器清除包子
System.out.println("包子吃完了");
flag = !flag;
this.notify();//吃完再唤醒厨师做包子
}
//做包子
public synchronized void doBaoZi(){
if (flag){//有包子时等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
baoZi = new BaoZi();
System.out.println("包子做好了");
flag = !flag;
this.notify();//做好唤醒吃货吃包子
}
}
//资源类,包子
class BaoZi{
}
11. 线程池
- 线程池是提前开辟好了几条线程,等线程任务执行完毕之后线程返回线程池,执行线程池的其他任务,而不是被销毁
- 原来每个任务都要单独开辟一个线程去执行,执行完后线程即销毁,在程序执行过程中频繁地创建和销毁线程会极大影响效率
- 所有线程在一个线程池中,方便管理
- 提前创建好线程——把任务丢入线程池执行——所有任务完成时关闭线程池
线程池使用案例
public class ThreadPool {
public static void main(String[] args) {
//线程池中的线程执行完之后不会销毁,可以重复使用
//线程池通过java.util.concurrent.Executors类创建
ExecutorService executorService = Executors.newFixedThreadPool(2);
//把任务加到线程池中去执行
executorService.submit(new MyRunnable());
executorService.submit(new MyRunnable());
executorService.submit(new MyRunnable());
//第二种方法
executorService.execute(new MyRunnable());
executorService.execute(new MyRunnable());
//关闭线程池
executorService.shutdown();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}

测试结果表明,线程池中的线程可以重用!
扩展——代理模式
Tread类本身就实现了Runnable接口,代理了一个Runnable实例target,它本身的run方法时空的,保证了没有任务的线程可以正常终止,这个target就是我们自己实现的Runnable接口,在进入Tread的run方法的时候,判断,如果target不是空,则调用target的run方法
//java.lang.Thread private Runnable target; @Override public void run() { if (target != null) { target.run(); } } private void init(...Runnable target...) { this.target = target; } public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }Java程序并不能直接与操作系统交互开启线程,而是调用一个native修饰的外部方法stat0来开启线程,而调用这个方法的方法是start(),所以开启线程需要使用new Tread().start,所以nerw Tread().run并不是开启线程的方法,虽然产生了结果,但他其实是执行在本线程中的,而不是开启一个新线程去执行这个方法;
start0()是将线程放入等待队列中,等待CPU调度,所以不一定会立即执行这个线程;
//java.lang.Thread public synchronized void start() { ... boolean started = false; try { start0(); started = true; } finally {...} ... } private native void start0();

浙公网安备 33010602011771号