Java多线程(四)
线程数据传递、线程安全、线程通信
线程数据传递
在传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。
- 通过构造方法传递数据
在调用start方法之前通过线程类的构造方法将数据传入线程。
package mythread;
public class MyThread1 extends Thread
{
private String name;
public MyThread1(String name)
{
this.name = name;
}
public void run()
{
System.out.println("hello " + name);
}
public static void main(String[] args)
{
Thread thread = new MyThread1("world");
thread.start();
}
}
- 通过变量和方法传递数据
向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。下面的代码是对MyThread1类的改版,使用了一个setName方法来设置 name变量:
package mythread;
public class MyThread2 implements Runnable
{
private String name;
public void setName(String name)
{
this.name = name;
}
public void run()
{
System.out.println("hello " + name);
}
public static void main(String[] args)
{
MyThread2 myThread = new MyThread2();
myThread.setName("world");
Thread thread = new Thread(myThread);
thread.start();
}
}
- 通过回调函数传递数据
上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。对于线程来说,是被动接收这些数据的。
package mythread;
class Data
{
public int value = 0;
}
class Work
{
public void process(Data data, Integer numbers)
{
for (int n : numbers)
{
data.value += n;
}
}
}
public class MyThread3 extends Thread
{
private Work work;
public MyThread3(Work work)
{
this.work = work;
}
public void run()
{
java.util.Random random = new java.util.Random();
Data data = new Data();
int n1 = random.nextInt(1000);
int n2 = random.nextInt(2000);
int n3 = random.nextInt(3000);
work.process(data, n1, n2, n3); // 使用回调函数
System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"
+ String.valueOf(n3) + "=" + data.value);
}
public static void main(String[] args)
{
Thread thread = new MyThread3(new Work());
thread.start();
}
}
线程通信
- 通信常用方法:
- wait() 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
- notify 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程,就唤醒优先级高的线程
- notifyAll 一旦执行此方法,就会唤醒所有被wait()的线程
- 使用前提:这三个方法均只能使用在同步代码块或者同步方法中。
package com.example.paoduantui.Thread;
/**
* 线程通信的例子:使用两个线程打印1—100,线程1,线程2交替打印
*
* 当我们不采取线程之间的通信时,无法达到线程1,2交替打印(cpu的控制权,是自动分配的)
* 若想达到线程1,2交替打印,需要:
* 1.当线程1获取锁以后,进入代码块里将number++(数字打印并增加)操作完以后,为了保证下个锁为线程2所有,需要将线程1阻塞(线程1你等等wait())。(输出1,number为2)
* 2.当线程2获取锁以后,此时线程1已经不能进入同步代码块中了,所以,为了让线程1继续抢占下一把锁,需要让线程1的阻塞状态取消(通知线程1不用等了notify()及notifyAll()),即应该在进入同步代码块时取消线程1的阻塞。
*
* */
class Number implements Runnable{
private int number = 1;//设置共享数据(线程之间对于共享数据的共享即为通信)
//对共享数据进行操作的代码块,需要线程安全
@Override
public synchronized void run() {
while(true){
//使得线程交替等待以及通知交替解等待
notify();//省略了this.notify()关键字
if(number<100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
public class CommunicationTest {
public static void main(String[] args){
//创建runnable对象
Number number = new Number();
//创建线程,并实现runnable接口
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
//给线程设置名字
t1.setName("线程1");
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
}
}
wait()/ notify()/ notifayAll():此三个方法定义在Object类中的,因为这三个方法需要用到锁,而锁是任意对象都能充当的,所以这三个方法定义在Object类中。
wait(在进入锁住的区域以后阻塞等待,释放锁让别的线程先进来操作)---- Obj.wait 进入Obj这个锁住的区域的线程把锁交出来原地等待通知
notify(由于有很多锁住的区域,所以需要将区域用锁来标识,也涉及到锁) ----- Obj.notify 新线程进入Obj这个区域进行操作并唤醒wait的线程
所以wait,notify需要使用在有锁的地方,也就是需要用synchronize关键字来标识的区域,即使用在同步代码块或者同步方法中,且为了保证wait和notify的区域是同一个锁住的区域,需要用锁来标识,也就是锁要相同的对象来充当
死锁
- 概念:
线程死锁的理解:死锁,两者都进入阻塞
出现死锁以后,不会出现提示,只是所有线程都处于阻塞状态,无法继续 - 解决办法:
1.减少同步共享变量
2.采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题
3.减少锁的嵌套。
线程安全问题的解决
1.先判断是否多线程
2.再判断是否有共享数据
3.是否并发的对共享数据进行操作
4.选择上述三种方法解决线程安全问题
package com.example.paoduantui.Thread;
/***
* 描述:甲乙同时往银行存钱,存够3000
*
*
* */
//账户
class Account{
private double balance;//余额
//构造器
public Account(double balance) {
this.balance = balance;
}
//存钱方法
public synchronized void deposit(double amt){
if(amt>0){
balance +=amt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:"+balance);
}
}
}
//两个顾客线程
class Customer extends Thread{
private Account acct;
public Customer(Account acct){
this.acct = acct;
}
@Override
public void run() {
for (int i = 0;i<3;i++){
acct.deposit(1000);
}
}
}
//主方法,之中new同一个账户,甲乙两个存钱线程。
public class AccountTest {
public static void main(String[] args){
Account acct = new Account(0);
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
- 单例模式的懒汉式的线程安全问题
- 单例:只能通过静态方法获取一个实例,不能通过构造器来构造实例
- 假设有多个线程调用此单例,而调用的获取单例的函数作为操作共享单例的代码块并没有解决线程的安全问题,会导致多个线程都判断实例是否为空,此时就会导致多个实例的产生,也就是单例模式的线程安全问题。
- 解决线程安全问题的思路:
将获取单例的方法改写成同步方法,即加上synchronized关键字,此时同步锁为当前类本身。(当有多个线程并发的获取实例时,同时只能有一个线程获取实例),解决了单例模式的线程安全问题。
用同步锁包裹住同步代码块的方式。
package com.example.paoduantui.Thread;
public class Bank {
//私有化构造器
private Bank(){}
//初始化静态实例化对象
private static Bank instance = null;
//获取单例实例,此种懒汉式单例模式存在线程不安全问题(从并发考虑)
public static Bank getInstance(){
if(instance==null){
instance = new Bank();
}
return instance;
}
//同步方法模式的线程安全
public static synchronized Bank getInstance1(){
if(instance==null){
instance = new Bank();
}
return instance;
}
//同步代码块模式的线程安全(上锁)
public static Bank getInstance2(){
synchronized (Bank.class){
if(instance==null){
instance = new Bank();
}
return instance;
}
}
//效率更高的线程安全的懒汉式单例模式
/**
* 由于当高并发调用单例模式的时候,类似于万人夺宝,只有第一个进入房间的人才能拿到宝物,
* 当多个人进入这个房间时,第一个人拿走了宝物,也就另外几个人需要在同步代码块外等候,
* 剩下的人只需要看到门口售罄的牌子即已知宝物已经被夺,可以不用进入同步代码块内,提高了效率。
*
*
* */
public static Bank getInstance3(){
if (instance==null){
synchronized (Bank.class){
if(instance==null){
instance = new Bank();
}
}
}
return instance;
}
}
参考文章
https://blog.csdn.net/gf771115/article/details/51682561
https://blog.csdn.net/weixin_44797490/article/details/91006241