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关键字可以作用于代码块、方法、类,不管怎样它的功能都是给对象加锁。
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号括起来的代码,作用的对象是调用这个代码块的对象;
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笔记(19)--- 线程概述;如何实现多线程并发;线程生命周期;Thread常用方法;终止线程的三种方式;线程安全问题;synchronized 实现同步线程模型;