Java多线程与并发库7.多个线程之间共享数据的方式探讨

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程与并发库7.多个线程之间共享数据的方式探讨相关的知识,希望对你有一定的参考价值。


多个线程访问共享对象和数据的方式
1.如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个
Runnable对象中有一个共享数据,例如售票系统就可以这么做这么做。

2.如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,有
如下两种方式来实现这些Runnable对象之间的数据共享:
(1)将数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象。
每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对
该数据进行的各个操作的互斥和通信。

(2)将这些Runnable对象作为某一类中的内部类,共享数据作为外部类中的成员变量,
每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操
作的互斥和通信。作为内部类的各个Runnable对象调用外部类的这些方法。

(3)上面两种方式的组合:将共享数据封装在另一个对象中,每个线程对共享数据的操作
方法也分配到那个对象身上去完成,对象作为这个外部对象中的成员内部类或局部内部类。

总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类
中,这样比较容易实现他们之间的同步互斥通信。

我们通过完成下边这个面试题来实践该理论:
问题:设计4个线程,其中两个线程每次对j增加1,
另外两个线程对j每次减少1。写出线程。

下面先是我自己的写法:

package cn.edu.hpu.test;

public class ThreadTest8

private static int j = 0;

public static void main(String[] args)
R1 r1=new R1();
R2 r2=new R2();
new Thread(r1).start();
new Thread(r1).start();
new Thread(r2).start();
new Thread(r2).start();



static class R1 implements Runnable

public synchronized void run()
j=j+1;
System.out.println(Thread.currentThread().getName()
+":j增加1,变为:"+j);




static class R2 implements Runnable

public synchronized void run()
j=j-1;
System.out.println(Thread.currentThread().getName()
+":j减去1,变为:"+j);




效果:


【Java多线程与并发库】7.多个线程之间共享数据的方式探讨_共享数据



我的思路就是开线程的时候,给run方法加一个线程锁,每一个线程操作j的时候都是


独立的不被打断的。



为了符合一开始的理论,我把j封装到一个Class里面使用:


package cn.edu.hpu.test;

public class ThreadTest8

private static Data data = new Data();

public static void main(String[] args)
data.setJ(0);
R1 r1=new R1(data);
R2 r2=new R2(data);
new Thread(r1).start();
new Thread(r1).start();
new Thread(r2).start();
new Thread(r2).start();



static class R1 implements Runnable

private Data data;
public R1(Data data)
this.data=data;


public synchronized void run()
int j=data.getJ();
data.setJ(j+1);
System.out.println(Thread.currentThread().getName()
+":j加上1,变为:"+data.getJ());




static class R2 implements Runnable

private Data data;
public R2(Data data)
this.data=data;


public synchronized void run()
int j=data.getJ();
data.setJ(j-1);
System.out.println(Thread.currentThread().getName()
+":j减去1,变为:"+data.getJ());






class Data

private int j;

public int getJ()
return j;


public void setJ(int j)
this.j = j;


这里在创建Runnable的时候,将Data对象传入进去。



下面的代码和上面的代码的区别是,第一段代码的线程是取共用的全局变量中去取数据,


而第二段代码的线程是根据传进来的对象来进行数据的操作,而这个对象是一个共用的全局变量。



但是实际上上面两个方法是有一个严重的问题的,就是数据同步问题。


反复运行上面的代码,会出现类似这种情况:


【Java多线程与并发库】7.多个线程之间共享数据的方式探讨_System_02



表面上看每一个线程的Run方法都是加了锁的,但是单单只能保证每一个Runnable在


操作数据的时候不会被打断,但是j的操作权是开放的,别的线程也可以同时修改j的值,


所以这里没有真正实现互斥。



解决办法就是,在要被操作的类中进行加锁,每一个线程在操作这个类的时候,j是否能被加还


是被减的权利是j所在的类而决定的,就是j的提供一个给j加1和给j减1的方法,然后全部加锁,


这样就保证一个线程在改变j的数据的时候,因为方法加锁,所以另外一个线程无法去改变这个值。



最终代码:


package cn.edu.hpu.test;

public class ThreadTest8

private static Data data = new Data();

public static void main(String[] args)
data.setJ(0);
R1 r1=new R1(data);
R2 r2=new R2(data);
new Thread(r1).start();
new Thread(r1).start();
new Thread(r2).start();
new Thread(r2).start();



static class R1 implements Runnable

private Data data;
public R1(Data data)
this.data=data;


public synchronized void run()
data.add();




static class R2 implements Runnable

private Data data;
public R2(Data data)
this.data=data;


public synchronized void run()
data.minus();






class Data

private int j;

public void setJ(int j)
this.j=j;


public synchronized void add()
j++;
System.out.println(Thread.currentThread().getName()
+":j加上1,变为:"+j);


public synchronized void minus()
j--;
System.out.println(Thread.currentThread().getName()
+":j减去1,变为:"+j);

结果:


【Java多线程与并发库】7.多个线程之间共享数据的方式探讨_Data_03


【Java多线程与并发库】7.多个线程之间共享数据的方式探讨_System_04

【Java多线程与并发库】7.多个线程之间共享数据的方式探讨_共享数据_05

【Java多线程与并发库】7.多个线程之间共享数据的方式探讨_Data_06

可以看到,不论怎么运行,j的值都会平稳的改变。


所以我们按照上面的方式操作就会真正实现多个线程共享数据。


​​​


Java 多线程与并发(案例 + 应用)

文章目录

1. 传统 创建线程的两种方式


一种继承Thread,一种是实现Runnable两种方式。

问题:如果匿名类实现了Runnable又覆盖了Thread的run方法,会执行谁的run方法?

public static void main(String[] args) 
	new Thread(new Runnable() 

		public void run() 
			// TODO Auto-generated method stub
			System.out.println("Runnable:执行了runnable");
		
		
	) 
		@Override
		public void run() 
			// TODO Auto-generated method stub
			System.out.println("Thread:执行了run方法");
		
	.start();
	
	//因为后来重写的run()方法覆盖了。因此这里执行的是Thread的run方法。

2. 传统 定时器技术


定时器的使用:

public static void main(String[] args) throws IOException, InterruptedException 

	new Timer().schedule(new TimerTask() 
		@Override
		public void run() 

			System.out.println("炸弹定时器1~~~~");
			new Timer().schedule(new TimerTask() 
				@Override
				public void run() 
					System.out.println("炸弹定时器2~~~");
				
			,2000);

			//如果我们this承接当前类的timerTask就会报错!!
			new Timer().schedule(this,2000);
		
	,2000);


为了解决上面的问题,我们可以通过创建对象的形式来解决:

class MyTimerTask extends TimerTask
	@Override
	public void run() 
		//我们可以直接new一个对象来重复调用就可以了。
		new Timer().schedule(new MyTimerTask(),2000);
	

3. 传统线程 互斥技术


synchronized 同步锁:

  • 同步监听器,就是要上锁的对象!!一般设置为this。

对于同步监听器的理解,就是类似一种锁机制。多个线程调用的时候,经过synchronized同步锁,先是查看同步监听器有没有被上锁抢占。如果有,就等待;如果没有,就抢占。(重点!!!)

也就说同步监听器是多个线程共有的一个东西,自然this就是最好的选择。this就是指的就是当前对象。

如果同步监听器设置为了对象的class,给class字节码对象上了锁,那么就算是多个对象情况下,也会被同步锁阻塞。因为,对象.class文件被上锁了,本身就没法使用class文件调用了。

import com.itholmes.config.User;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scheduling.support.CronTrigger;
import sun.applet.Main;

import java.io.*;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;

public class MainTest 
	public static void main(String[] args) throws IOException, InterruptedException 
		new MainTest().init();
	

	private void init()

		//这里因为Outputer是内部类,不能直接再static方法里面显示,因此我们就可以通过创建一个方法,通过对象来调用。
		Outputer outputer = new Outputer();

		new Thread(new Runnable() 
			@Override
			public void run() 
				while (true)
					try 
						Thread.sleep(10);
					 catch (InterruptedException e) 
						e.printStackTrace();
					
					outputer.output("123456");

					// 这样就不可以了!!!就算有多个线程都是调用,他们都不是用的同一个outpuer对象。
					// 也就是多线程需要加锁的地方就是多个线程同时操作一个对象或者一部分代码的时候的情况。
					//new Outputer().output("123123");
				
			
		).start();

		new Thread(new Runnable() 
			@Override
			public void run() 
				while (true)
					try 
						Thread.sleep(10);
					 catch (InterruptedException e) 
						e.printStackTrace();
					
					outputer.output("2344");
				
			
		).start();

	

	//了解静态内部类 和 普通内部类一些区别:https://blog.csdn.net/qq_37768971/article/details/101166164
	static class Outputer

		//方法synchronized的同步监听器是this,这里的this指向的是当前output对象。
		public synchronized void output(String name)

			int len = name.length();

			for (int i=0;i<len;i++)
				System.out.print(name.charAt(i));
			
			System.out.println();

		

		//同样,方法synchronized的同步监听器是this,然而这里的this指向的是OutPuter.class。
		public static synchronized void output2(String name)

			int len = name.length();

			for (int i=0;i<len;i++)
				System.out.print(name.charAt(i));
			
			System.out.println();

		

	


4. 传统线程 同步通信技术


解决一个问题:

public class ThreadTest 

    public static void main(String[] args) 

        /**
         *  对于同步的多线程:
         *      第一点:一般都是通过一个类,就像下面的business对象一样,我们直接将同步锁 和 wait ,notify等封装到对象方法里面。
         *      这样无论是同步技术实现,还是同步监听器都好设置。
         *
         *      第二点:同步锁要写到类中,不要写到线程的run方法中。
         *
         *      第三点:一定用while,不要用if。
         */
        Business business = new Business();

        new Thread(new Runnable() 
            @Override
            public void run() 
                for (int j = 0;j<50;j++)
                    business.sub(j);
                
            
        ).start();

        for (int j = 0;j<50;j++)
            business.main(j);
        

    



class Business

    private Boolean bShouldSub = true;

    public synchronized void sub(int j)

        //用while,不要用if , while有循环效果!if有可能伪唤醒。
        //if (!bShouldSub)
        while (!bShouldSub)
            try 
                this.wait();
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
        for (int i=0;i<10;i++)
            System.out.println("50循环的第"+j+",子线程第"+i+"次循环。");
        
        bShouldSub = false;
        this.notifyAll();
    
    public synchronized void main(int j)

        //用while,不要用if , while有循环效果!if有可能伪唤醒。
        while (bShouldSub)
            try 
                this.wait();
             catch (InterruptedException e) 
                e.printStackTrace();
            
        

        for (int i = 0; i < 100; i++) 
            System.out.println("100循环的第" + j + ",主线程第" + i + "次循环。");
        
        bShouldSub = true;
        this.notifyAll();
    

重点:

  • 第一点:一般都是通过一个类,就像下面的business对象一样我们直接将同步锁 和 wait ,notify等封装到对象方法里面。这样无论是同步技术实现,还是同步监听器都好设置。
  • 第二点:同步锁要写到类中,不要写到线程的run方法中。
  • 第三点:一定用while,不要用if。用if就有可能伪唤醒。

5. 线程范围内 共享变量 概念和作用


理解下面的data 和 map:

  • 也就是通过map存thread的方式来达到共享变量。
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

public class ThreadScopeShareData 

    //private static int data = 0; 如果有这种static成员变量就容易出现共享混淆的一个问题。

    //这样我们就可以通过Thread + xxx 的方法来实现,这也就是数据库池经常用到的一种方式。
    private static Map<Thread,Integer> threadData = new HashMap<>();

    public static void main(String[] args) 
        for (int i=0;i<2;i++)
            new Thread(new Runnable() 
                @Override
                public void run() 
                    //让data随机等于一个int类型数据。
                    int data = new Random().nextInt();
                    
                    System.out.println(Thread.currentThread().getName()
                            + "has put data: " + data);

                    threadData.put(Thread.currentThread(),data);
                    new A().get();
                    new B().get();
                
            ).start();
        
    

    static class A 
        public void get()
            Integer data = threadData.get(Thread.currentThread());
            System.out.println("A from" + Thread.currentThread().getName() + " get data: "+data);
        
    

    static class B
        public void get()
            Integer data = threadData.get(Thread.currentThread());
            System.out.println("B from" + Thread.currentThread().getName() + " get data: "+data);
        
    


6. ThreadLocal类及应用技巧


ThreadLocal使用起来,就是多线程使用的时候,每个线程都会操作自己线程对应的那个值。

ThreadLocal原理:

  • 就是key + value 的map,key是各自的线程对象本身。每次get获取就要通过线程对象来获取(注意:并不是将线程对象最为参数传递)。这就是一种容器效果。
  • 由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

ThreadLocal.clear()方法可以清空,释放内存。

(remove方法可以拿掉当前线程的内容。)


import java.util.Random;

public class ThreadScopeShareData 

    /**
     * ThreadLocal类,多线程之间是不会相互冲突的。
     * 这里是有泛型的,不仅仅用于integer。
     */
    static ThreadLocal<Integer> x = new ThreadLocal();
    static ThreadLocal<MyThreadScopeData> myThreadScopeData = new ThreadLocal<>();

    public static void main(String[] args) 
        for (int i=0;i<2;i++)

            new Thread(new Runnable() 
                @Override
                public void run() 
                    //让data随机等于一个int类型数据。
                    int data = new Random().nextInt();
                    
                    System.out.println(Thread.currentThread().getName()
                            + "has put data: " + data);

                    //这里的是当前线程内定义的data。
                    x.set(data);

                   MyThreadScopeData.getInstance().setName("name"+data);
                   MyThreadScopeData.getInstance().setAge(data);

                    new A().get();
                    new B().get();
                

            ).start();
        
    

    static class A 
        public void get()
            //这里获取就是当前线程获取的数据。
            Integer data = x.get();
            System.out.println("A from" + Thread.currentThread().getName() + " get data: "+data);

            MyThreadScopeData myData = MyThreadScopeData.getInstance();
            System.out.println("A from " + Thread.currentThread().getName() + "getMyData: " + myData.getName() + ","
            + myData.getAge());
        
    

    static class B
        public void get()
            Integer data = x.get();
            System.out.println("B from" + Thread.currentThread().getName() + " get data: "+data);

            MyThreadScopeData myData = MyThreadScopeData.getInstance();
            System.out.println("B from " + Thread.currentThread().getName() + "getMyData: " + myData.getName() + ","
                    + myData.getAge());
        
    



class MyThreadScopeData

    private MyThreadScopeData()

    private static ThreadLocal<Java 多线程与并发(案例 + 应用)

高并发多线程基础之线程间通信与数据共享及其应用

Java核心技术-并发

Java 多线程与并发:内存模型

(黑马Java多线程与并发库高级应用)05 线程范围内共享变量的概念与作用

Java多线程基础