Java 多线程(1): 基础知识概述与synchronized关键字

Java 多线程(1): 基础知识概述与synchronized关键字

前言

本篇介绍一些多线程的基本知识,主要学习、参考资料《Java并发编程实战》,视频:千锋2020新版_Java多线程详解(Java基础课程)

多线程必知必会

1、多线程程序运行的特点

  • 随机运行

直接上代码

public class MyThread extends Thread {
    @Override
    public void run() {
        for(int i=1;i<=10;i++){
            System.out.println("子线程 i:"+i);
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {
        MyThread myThread=new MyThread();
        myThread.start();
        for(int i=1;i<=10;i++){
            System.out.println("主线程 i:"+i);
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果

在这里插入图片描述

我们可以主线程和子线程是交替执行的,但是谁先谁后,这是随机的!主线程和子线程在争夺CPU时间片,谁抢到了谁执行(之前操作系统没白学)。

所以第一点,线程运行代码是随机的,并不是按代码编写顺序谁先谁后。

实际上这个问题在js里面很常见,所以js出了then、async/await来解决这个问题。

  • 变量共享(竞态条件)

    共享变量以及非原子操作导致竞态条件,比如a++,实际上是三个操作,a=x,k=x+1,a=k;在a=x、k=x+1两条指令之间是有时间间隔的,同理k=x+1、a=k之间也有,结果就是在多线程条件下出现错误。(这里如果有进程管理的知识可以想的更明白,如何进程切换、线程切换)

在这里插入图片描述

那么如何共享变量呢,首先必须是一个对象的成员变量才可以!,使用Runnable接口方式实现多线程更容易实现共享变量,继承Thread类方式实现共享变量就我目前知道的方法是生产者——消费者模式。

2、四种实现多线程的方法

  • 实现Runnable接口
  • 继承Thread类
  • 实现Callable接口
  • 线程池

作为基础知识是只知道前两种即可,后两种我也不会(很快就会了)。

实现Runnable接口还有实现Callable接口的方式最后还是要依靠Thread类。暂时只说前两个。

实现Runnable接口方式(直接上综合的代码):

//银行卡
public class Card {
    private double money;
    private boolean flag=false;//true 有钱 false没钱

    public synchronized void save(double m){
        while(flag){
            try {
                this.wait();//进入等待队列,同时释放锁和cpu 锁.wait()
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        money+=m;
        System.out.println(Thread.currentThread().getName()+"存了"+m+",余额是"+money);
        flag=true;
        this.notifyAll();//唤醒 锁.notify()
    }

    public synchronized void get(double m){
        while(!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        money-=m;
        System.out.println(Thread.currentThread().getName()+"取了"+m+",余额是"+money);
        flag=false;
        this.notifyAll();
    }
}
/*
* 取钱(消费者)
*/
public class GetTest implements Runnable {
    private Card card;

    public GetTest(Card card) {
        this.card = card;
    }

    @Override
    public void run() {
        for (int i=0;i<10;i++){
            card.get(1000);
        }
    }
}
/*
* 存钱(生产者)
*/
public class SaveTest implements Runnable {
    private Card card;

    public SaveTest(Card card) {
        this.card = card;
    }

    @Override
    public void run() {
        for (int i=0;i<10;i++){
            card.save(1000);
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Card card=new Card();
        SaveTest saveTest=new SaveTest(card);
        GetTest getTest=new GetTest(card);

        Thread thread=new Thread(saveTest,"存钱");
        Thread thread1=new Thread(getTest,"取钱");
//多存多取——》if换成while但是会死锁——》解决方法notifyAll()
        Thread thread2=new Thread(saveTest,"存钱2号");
        Thread thread3=new Thread(getTest,"取钱2号");
        thread.start();
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

上面是一个经过同步处理的存钱取钱多线程案例,同时它也是一个生产者——消费者的案例,如果是通过继承Thread类该怎么写呢。

//Card类不变
/*
* 消费者类
*/
public class GetTest extends Thread{
    private Card card;

    public GetTest(Card card) {
        this.card = card;
    }

    @Override
    public void run() {
        for (int i=0;i<10;i++){
            card.get(1000);
        }
    }
}
/*
* 生产者类
*/
public class SaveTest extends Thread {
    private Card card;

    public SaveTest(Card card) {
        this.card = card;
    }

    @Override
    public void run() {
        for (int i=0;i<10;i++){
            card.save(1000);
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Card card=new Card();
        SaveTest saveTest=new SaveTest(card);
        GetTest getTest=new GetTest(card);
		SaveTest saveTest1=new SaveTest(card);
        GetTest getTest1=new GetTest(card);
        
        saveTest.start();
        getTest.start();
        saveTest1.start();
        getTest1.start();
    }
}

上面提到了用实现Runnable接口的方式更容易实现变量共享,因为实现Runnable接口的类只用创建一个对象然后多创建几个Thread对象就是了,而继承Thread类的方式是多创建几个实际业务的对象,或者把成员变量用static修饰,但是用Runnable接口明显好一点。

如果不是生产者消费者的模式。

public class Card extends Thread {
    private double money;

    @Override
    public void run() {
        get(5);
        save(5);
    }

    private void save(double money) {
        this.money+=money;
        System.out.println(Thread.currentThread().getName()+"余额:"+this.money);
    }

    private void get(double money) {
        this.money-=money;
        System.out.println(Thread.currentThread().getName()+"余额:"+this.money);
    }
}
public class Test  {
    public static void main(String[] args) {
        Card card=new Card();
        Card card1=new Card();
        card.start();
        card1.start();
    }
}

因为是两个对象,变量当然是无法共享的。

synchronized关键字

synchronized关键字可以作用于代码块、方法、类,不管怎样它的功能都是给对象加锁。

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

看代码:

public class Card {
    private double money;

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
}
public class Test {
    public static void main(String[] args) {
        final Card card=new Card();
        /*
        * 存钱
        * */
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<10;i++){
                    synchronized (card) {
                        card.setMoney(card.getMoney() + 1000);
                        System.out.println(Thread.currentThread().getName() + "存了1000,余额是+" + card.getMoney());
                    }
                    }
            }
        };
        /*
        * 取钱
        * */
        Runnable runnable1=new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<10;i++) {
                    synchronized (card) {
                        if (card.getMoney() >= 1000) {
                            card.setMoney(card.getMoney() - 1000);
                            System.out.println(Thread.currentThread().getName() + "取了1000,余额是+" + card.getMoney());
                        } else {
                            System.out.println("余额不足");
                        }
                    }
                }
            }
        };
        Thread thread=new Thread(runnable,"张三存钱");
        Thread thread1=new Thread(runnable1,"张三取钱");
        thread.start();
        thread1.start();
    }
}

  • 修饰代码块

    上面演示了如何给代码块用synchronized,哪个线程获得了card对象谁可以执行代码块,但是没有加锁的部分,任意线程都可以执行。

  • 修饰方法

    在“四种实现多线程的方法”中的代码示例是对方法加锁,锁是this对象。但是注意:在定义接口方法时不能使用synchronized关键字。构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。

  • 修饰静态方法

  • 修饰类

    修饰静态方法和修饰类放在一起说,它们锁的是.class,类(型)在整个JVM里面只有一份,所以哪个线程有加锁的类型谁执行。

这里面类锁挺不好理解的,唉。

还有死锁、线程通信就不说了,确实多线程感觉比较难,看书也不太懂,继续努力了。

写在最后

根据我的理解:不要自己去使用多线程,写Android除外。

因为用java写服务端程序,程序所处的环境本身就是多线程的,有很多用户同时访问,如果在多线程环境中我们再创建多线程,那就是自己给自己找麻烦,我们能做的就是尽量写“好”的代码,在多线程环境不出现错误。

最简单的方法就是不出现竞态条件,不去变量共享,如果必须做这些那只能是加锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值