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

Posted 明天会更好new

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 多线程: 基础知识概述与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写服务端程序,程序所处的环境本身就是多线程的,有很多用户同时访问,如果在多线程环境中我们再创建多线程,那就是自己给自己找麻烦,我们能做的就是尽量写“好”的代码,在多线程环境不出现错误。

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

以上是关于Java 多线程: 基础知识概述与synchronized关键字的主要内容,如果未能解决你的问题,请参考以下文章

Java 并发编程 -- 并发编程线程基础(线程安全问题可见性问题synchronized / volatile 关键字CASUnsafe指令重排序伪共享Java锁的概述)

Java 并发编程 -- 并发编程线程基础(线程安全问题可见性问题synchronized / volatile 关键字CASUnsafe指令重排序伪共享Java锁的概述)

java 多线程——quartz 定时调度的例子

JAVA笔记(19)--- 线程概述;如何实现多线程并发;线程生命周期;Thread常用方法;终止线程的三种方式;线程安全问题;synchronized 实现同步线程模型;

Java中Synchronized的用法

Java中Synchronized的用法