从0开始深入理解并发编程(二)

本文深入探讨了Java线程间的通信与协作,重点关注了synchronized关键字,包括其概念、用法分类、对象锁与类锁的区别,并通过代码实战展示了加锁的常见误区。此外,还讨论了volatile的轻量级同步机制以及wait/notify的等待通知机制,强调了在使用中需要注意的问题和最佳实践。
摘要由CSDN通过智能技术生成

1. 线程间的通信和协调、 协作

  线程开始运行, 拥有自己的栈空间, 就如同一个脚本一样, 按照既定的代码一步一步地执行, 直到终止。但是, 每个运行中的线程, 如果仅仅是孤立地运行, 那么没有一点儿价值,或者说价值很少。如果多个线程能够相互配合完成工作,这就离不开线程间的通信和协调、 协作,包括数据之间的共享,协同处理事情,这将会带来巨大的价值。

1.1. 管道输入输出流

  我们已经知道,进程间有好几种通信机制,其中包括了管道,其实 Java 的线程里也有类似的管道机制, 用于线程之间的数据传输, 而传输的媒介为内存。

设想这么一个应用场景:通过 Java 应用生成文件, 然后需要将文件上传到云端,比如:

1 、页面点击导出后, 后台触发导出任务, 然后将 mysql 中的数据根据导出条件查询出来, 生成 Excel 文件, 然后将文件上传到 oss,最后发布一个下载文件的链接。

2、和银行以及金融机构对接时,从本地某个数据源查询数据后, 上报 xml 格式的数据,给到指定的 ftp、或是 oss 的某个目录下也是类似的。

我们一般的做法是, 先将文件写入到本地磁盘, 然后从磁盘读出文件上传到云盘,但是通过 Java 中的管道输入输出流一步到位,则可以避免写入磁盘这 一步。

Java 中的管道输入/输出流主要包括了如下 4 种具体实现:

管道字节流:PipedOutputStream 、PipedInputStream ;管道字符流:PipedReader 和 PipedWriter

/**
 * main线程和PrintThread线程之间传输数据
 */
public class Pipeline {

    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        /* 将输出流和输入流进行连接,否则在使用时会抛出IOException*/
        out.connect(in);
        Thread printThread = new Thread(new Print(in), "PrintThread");
        printThread.start();
        int receive = 0;
        try {
            /*将键盘的输入,用输出流接受,在实际的业务中,可以将文件流导给输出流*/
            while ((receive = System.in.read()) != -1){
                out.write(receive);
            }
        } finally {
            out.close();
        }
    }

    static class Print implements Runnable {

        private PipedReader in;

        public Print(PipedReader in) {
            this.in = in;
        }

        @Override
        public void run() {
            int receive = 0;
            try {
                /*输入流从输出流接收数据,并在控制台显示
                *在实际的业务中,可以将输入流直接通过网络通信写出 */
                while ((receive = in.read()) != -1){
                    System.out.print((char) receive);
                }
            } catch (IOException ex) {
            }
        }
    }
}

总结

管道流中,入口处是输出流,需要输出流往管道中写数据(往管道中输出数据)。出口处是输入流,需要输入流从管道中消费(读)数据。

1.2. join 方法

  join(),把指定的线程加入到当前线程, 可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A 的Join()方法, 直到线程 A 执行完毕后, 才会继续执行线程 B 剩下的代码。

/**
 *类说明:演示Join()方法的使用
 */
public class UseJoinDemo {

    public static void main(String[] args) throws Exception {
        System.out.println(Thread.currentThread().getName() + "线程开始执行...");

        Thread threadA = new Thread(new RunnableA());
        threadA.setName("ThreadA");

        Thread threadB = new Thread(new RunnableB(threadA));
        threadB.setName("ThreadB");

        threadA.start();
        threadB.start();

        threadB.join();

        Thread.sleep(2000);//让主线程休眠2秒
        System.out.println(Thread.currentThread().getName() + "线程执行完成...");
    }

    static class RunnableA implements Runnable {

        public void run() {
            System.out.println("ThreadA开始执行...");
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
            }

            System.out.println(Thread.currentThread().getName() + " 执行结束...");
        }
    }

    static class RunnableB implements Runnable {
        private Thread thread;

        public RunnableB(Thread thread) {
            this.thread = thread;
        }

        public RunnableB() {}

        public void run() {
            System.out.println("ThreadB开始执行...");
            try {
                if(thread!=null) thread.join();
//                Thread.sleep(2000);
            } catch (Exception e) {
            }
            System.out.println(Thread.currentThread().getName() + " 执行结束...");
        }
    }
    
}
/**
 *类说明:无Join时线程的表现
 */
public class NoUseJoin {

    public static void main(String[] args) throws Exception {
        System.out.println(Thread.currentThread().getName() + "线程开始执行...");

        Thread threadA = new Thread(new RunnableA());
        threadA.setName("ThreadA");

        Thread threadB = new Thread(new RunnableB());
        threadB.setName("ThreadB");

        threadA.start();
        threadB.start();

        Thread.sleep(2000);//让主线程休眠2秒
        System.out.println(Thread.currentThread().getName() + " 执行完成...");
    }

    static class RunnableA implements Runnable {

        public void run() {
            System.out.println("ThreadA开始执行...");
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
            }

            System.out.println(Thread.currentThread().getName() + " 执行结束...");
        }
    }

    static class RunnableB implements Runnable {

        private Thread thread;

        public RunnableB(Thread thread) {
            this.thread = thread;
        }

        public RunnableB() {}

        public void run() {
            System.out.println("ThreadB开始执行...");
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
            }
            System.out.println(Thread.currentThread().getName() + " 执行结束...");
        }
    }
}

面试题

现在有 T1、T2、T3 三个线程, 你怎样保证 T2 在 T1 执行完后执行, T3 在 T2 执行完后执行?

答:用 Thread#join 方法即可, 在 T3 中调用 T2.join,在 T2 中调用 T1.join。

1.3. synchronized 内置锁

  Java 支持多个线程同时访问一个对象或者对象的成员变量。但是,多个线程同时访问同一个变量,线程之间的执行顺序是不可预知的,会产生不可预料的结果。如下这段程序,每次执行后的结果是不固定的,这便是出现了线程安全问题。

public class SynDemo {

	private int count =0;

	public long getCount() {
		return count;
	}

	public void setCount(int count) {
		this.count = count;
	}

	public void incrementCountNoSyn(){
		count++;
	}

	//线程
	private static class Count extends Thread {
		private SynDemo synDemo;

		public Count(SynDemo synDemo) {
			this.synDemo = synDemo;
		}

		@Override
		public void run() {
			for (int i = 0; i < 10000; i++) {
				synDemo.incrementCountNoSyn();
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		SynDemo synDemo = new SynDemo();
		//启动两个线程
		Count count1 = new Count(synDemo);
		Count count2 = new Count(synDemo);
		count1.start();
		count2.start();
		Thread.sleep(50);
		System.out.println(synDemo.count);//20000?
	}
}

运行结果

12907

为此,Java提供了synchronized关键字来解决线程安全问题。我们先来看下使用synchronized关键字之后的效果

/**
 *类说明:synchronized关键字的使用方法
 */
public class SynDemo {

	private int count = 0;

	private Object obj = new Object();//作为一个锁

	public long getCount() {
		return count;
	}

	public void setCount(int count) {
		this.count = count;
	}

	/*无锁时*/
	public void incrementCountNoSyn(){
		count++;
	}

	/*synchronized直接放在方法上*/
	public synchronized void incrementCountMethod(){
		count++;
	}

	/*用在同步块上*/
	public void incrementCountBlock(){
		synchronized (this){
			count++;
		}
	}

	/*用在同步块上,但是锁的是单独的对象实例*/
	public void incrementCountObj(){
		synchronized (obj){
			count++;
		}
	}

	//线程
	private static class SyncThread extends Thread {
		private SynDemo synDemo;

		public SyncThread(SynDemo synDemo) {
			this.synDemo = synDemo;
		}

		@Override
		public void run() {
			for (int i = 0; i < 10000; i++) {
//				synDemo.incrementCountNoSyn();
				synDemo.incrementCountMethod();
//				synDemo.incrementCountBlock();
//				synDemo.incrementCountObj();
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		SynDemo synDemo = new SynDemo();
		//启动两个线程
		SyncThread syncThread1 = new SyncThread(synDemo);
		SyncThread syncThread2 = new SyncThread(synDemo);
		syncThread1.start();
		syncThread2.start();
		Thread.sleep(50);
		System.out.println(synDemo.count);//20000
	}
}

运行结果

20000

1.3.1. 概念​及特性

概念

  synchronized是java中的关键字,可以在需要线程安全的业务场景中进行使用,保证线程安全,它是利用锁机制来实现同步的。

  synchronized 可以修饰方法或者以同步块的形式进行使用,它主要确保多个线程在同一个时刻, 只能有一个线程处于方法或者同步块中(将并发的访问变成串行的访问),它保证了线程对变量访问的可见性和排他性, 使多个线程访问同一个变量的结果正确(保证线程安全),它又称为内置锁机制。

特性

  • 原子性:同一时间只允许一个线程持有某个对象锁,对需同步的代码块进行访问,是一种排他的机制。因此,被synchronized关键字修饰的同步代码块是无法被中途打断的,能保证代码的原子性;
  • 可见性:synchronized关键字包括monitor enter和monitor exit两个JVM命令,它能保证在任何时候任何线程,执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功之后,共享变量被更新后的值必须刷入主内存;
  • 有序性:synchronized的指令严格遵守java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter;这种顺序性是以程序的串行化执行换来的,在synchronized关键字所修饰的代码块中代码指令也会发生指令重排序的情况;

1.3.2. 用法分类

1. 按照修饰对象

 ● 修饰代码块

       synchronized(this|object) {}

       synchronized(类.class) {}

 ● 修饰方法

       修饰非静态方法

       修饰静态方法

2. 按照获取的锁分类

 ● 获取对象锁

       synchronized(this|object) {}

       修饰非静态方法

 ● 获取类锁

      synchronized(类.class) {}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值