多线程应用提高(I) 多线程常见问题、常用方法和关键字

  created  by  鱼鱼 {{tag}}
创建于 2019年01月29日 16:25:12 最后修改于 2019年12月07日 00:04:49

创建并启动一个多线程任务

    我们一般熟识的创建多线程方式即为继承Thread类或是实现Runnable接口,重写run()方法,还有创建线程池实现。

    手动定义一个线程任务(作为内部类)的方法现在已经不被提倡,所以遇到可能存在并发的复杂任务时,一般采用线程池来实现。


相关方法

    一些设计并发常用并且容易被混淆的方法们:

  • static sleep()    : Thread类的静态方法,阻塞当前正在线程,不释放锁;

  • wait()    : 当前线程暂停,并释放锁且暂时无法重新获得锁,必须绑定当前对象内容锁(如使用Synchronized的同步块),知道其他线程调用notify()/notifyAll()才有机会获得锁继续执行;

  • yield()    : 当前线程暂停,此时时间片分配给其他线程,但是不会分配给优先级更低的线程;

  • join()    : 等待此线程执行完毕后才能执行;

  • isInterrupted()    : 判断此线程是否执行完毕。

    有一些停止线程任务的方法已经被弃用,其实,从外部去中断一个线程是很不合理的方式,我们应该使用更加安全的方式去打断一个我们希望被停止运行线程,例如在循环中使用标志位。


synchronized

    我们借助synchronized来定义同步方法或是同步块,使用对象锁。有以下几点要注意:

  1. 定义的是对象锁,多个同步块拥有同一个对象锁才能被约束;

  2. 是可重入锁,抛出异常也会自动释放;

  3. 该关键字不会被继承;

  4. 通常使用 本类.class(一般用于类锁)、字符串、this、一个既有的非空对象作为锁;

  5. 在静态方法中或是.class定义为类锁,锁住的是类,与对象锁并不冲突。



关于volatile关键字

    该关键词定义在变量,主要为保证变量变化在主存的可见性保证数据的一致性,这里我们不再介绍jvm内存模型,需要注意的是,有些操作即使不适用volatile也能做到变量同步,例如sleep和输出操作,使用synchronized后我们可以避免使用volatile。


线程间的通信

    线程间的通信主要利用管道输入\输出流(PipedOutputStream、 PipedInputStream、PipedWriter、 PipedReader),下面是一个实例:

读线程

public class ThreadRead extends Thread {    
	private ReadData read;    
	private PipedInputStream input;    
	public ThreadRead(ReadData read, PipedInputStream input) {    
		super();    
		this.read = read;    
		this.input = input;    
	}    
	@Override    
	public void run() {    
		read.readMethod(input);    
	}    
}


写线程

public class ThreadWrite extends Thread {    
	private WriteData write;    
	private PipedOutputStream out;    
	public ThreadWrite(WriteData write, PipedOutputStream out) {    
		super();    
		this.write = write;    
		this.out = out;    
	}    
	@Override    
	public void run() {    
		write.writeMethod(out);    
	}    
}


主方法:

public static void main(String[] args) {
        try {
            WriteData writeData = new WriteData();
            ReadData readData = new ReadData();

            PipedInputStream inputStream = new PipedInputStream();
            PipedOutputStream outputStream = new PipedOutputStream();

            // inputStream.connect(outputStream);  连接管道两端,皆可
            outputStream.connect(inputStream);

            ThreadRead threadRead = new ThreadRead(readData, inputStream);
            threadRead.start();

            Thread.sleep(2000);

            ThreadWrite threadWrite = new ThreadWrite(writeData, outputStream);
            threadWrite.start();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }



  关于ThreadLocal

    ThreadLocal类是一个泛型类,用于解决多线程中变量共享的问题,可以让线程绑定一个自己独立的静态变量,注意,它的存在是区别于静态变量的,不要与费静态变量混为一谈。相关方法:

  • get() : 返回当前线程的此线程局部变量的副本中的值;

  • set(T value) : 将当前线程的此线程局部变量的副本设置为指定的值;

  • remove() 删除此线程局部变量的当前线程的值

  • initialValue():返回此线程局部变量的当前线程的“初始值”。

   

    其实现原理其实是在一个线程中创建ThreadLocalMap(基于HashMap),借助这种手段定义的静态变量对于每个线程都有其独立的副本,在免去其初始化性能损耗的同时,提供了安全性的操作,可以用于数据库连接管理等情形。

    一个简单的例子:

public class Test {
    public static ThreadLocal<String> str = new ThreadLocal<String>();

    public static void main(String[] args) {
        if (str.get() == null) {
            System.out.println("为ThreadLocal类对象放入值:str");
            str.set("string");
        }
        System.out.println(str.get());//string
    }

}


    一个很常见的应用就是工具类里用来定义SimpleDateFormat对象,因为这个对象本身不是线程安全的,容易引起数据串扰或是抛出异常。

    内存泄露问题

    Map中的key值是ThreadLocal变量的弱引用,仅仅针对key,此时ThreadLocal实例没有强引用指向,将会gc被回收,但是其在Map中的value不会被回收,直到线程结束整体被gc回收,这就存在了内存泄露的问题,尤其是在线程池中,线程不会被回收而是复用。

    为了避免这个问题,ThreadLocal在每次执行get(),set()时会将key为null的value回收,但是在线程池的环境中仍会出现问题,对此有解决方案:在每次使用完一个ThreadLocal变量后,执行remove()方法移除变量,使其值为null从而被gc回收,或是直接将其定义为private static。


其他并发常见问题

    有效规避死锁

    死锁不只存在于数据库中,两个线程相互等待彼此的资源是放也会产生死锁。

    死锁的产生条件:

  1. 互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放 

  2. 请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。 

  3. 不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用 

  4. 循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。


相关链接:线程间通信知识点补充

相关书籍:《Java并发编程的艺术

评论区
评论
{{comment.creator}}
{{comment.createTime}} {{comment.index}}楼
评论

多线程应用提高(I) 多线程常见问题、常用方法和关键字

多线程应用提高(I) 多线程常见问题、常用方法和关键字

创建并启动一个多线程任务

    我们一般熟识的创建多线程方式即为继承Thread类或是实现Runnable接口,重写run()方法,还有创建线程池实现。

    手动定义一个线程任务(作为内部类)的方法现在已经不被提倡,所以遇到可能存在并发的复杂任务时,一般采用线程池来实现。


相关方法

    一些设计并发常用并且容易被混淆的方法们:

    有一些停止线程任务的方法已经被弃用,其实,从外部去中断一个线程是很不合理的方式,我们应该使用更加安全的方式去打断一个我们希望被停止运行线程,例如在循环中使用标志位。


synchronized

    我们借助synchronized来定义同步方法或是同步块,使用对象锁。有以下几点要注意:

  1. 定义的是对象锁,多个同步块拥有同一个对象锁才能被约束;

  2. 是可重入锁,抛出异常也会自动释放;

  3. 该关键字不会被继承;

  4. 通常使用 本类.class(一般用于类锁)、字符串、this、一个既有的非空对象作为锁;

  5. 在静态方法中或是.class定义为类锁,锁住的是类,与对象锁并不冲突。



关于volatile关键字

    该关键词定义在变量,主要为保证变量变化在主存的可见性保证数据的一致性,这里我们不再介绍jvm内存模型,需要注意的是,有些操作即使不适用volatile也能做到变量同步,例如sleep和输出操作,使用synchronized后我们可以避免使用volatile。


线程间的通信

    线程间的通信主要利用管道输入\输出流(PipedOutputStream、 PipedInputStream、PipedWriter、 PipedReader),下面是一个实例:

读线程

public class ThreadRead extends Thread {    
	private ReadData read;    
	private PipedInputStream input;    
	public ThreadRead(ReadData read, PipedInputStream input) {    
		super();    
		this.read = read;    
		this.input = input;    
	}    
	@Override    
	public void run() {    
		read.readMethod(input);    
	}    
}


写线程

public class ThreadWrite extends Thread {    
	private WriteData write;    
	private PipedOutputStream out;    
	public ThreadWrite(WriteData write, PipedOutputStream out) {    
		super();    
		this.write = write;    
		this.out = out;    
	}    
	@Override    
	public void run() {    
		write.writeMethod(out);    
	}    
}


主方法:

public static void main(String[] args) {
        try {
            WriteData writeData = new WriteData();
            ReadData readData = new ReadData();

            PipedInputStream inputStream = new PipedInputStream();
            PipedOutputStream outputStream = new PipedOutputStream();

            // inputStream.connect(outputStream);  连接管道两端,皆可
            outputStream.connect(inputStream);

            ThreadRead threadRead = new ThreadRead(readData, inputStream);
            threadRead.start();

            Thread.sleep(2000);

            ThreadWrite threadWrite = new ThreadWrite(writeData, outputStream);
            threadWrite.start();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }



  关于ThreadLocal

    ThreadLocal类是一个泛型类,用于解决多线程中变量共享的问题,可以让线程绑定一个自己独立的静态变量,注意,它的存在是区别于静态变量的,不要与费静态变量混为一谈。相关方法:

   

    其实现原理其实是在一个线程中创建ThreadLocalMap(基于HashMap),借助这种手段定义的静态变量对于每个线程都有其独立的副本,在免去其初始化性能损耗的同时,提供了安全性的操作,可以用于数据库连接管理等情形。

    一个简单的例子:

public class Test {
    public static ThreadLocal<String> str = new ThreadLocal<String>();

    public static void main(String[] args) {
        if (str.get() == null) {
            System.out.println("为ThreadLocal类对象放入值:str");
            str.set("string");
        }
        System.out.println(str.get());//string
    }

}


    一个很常见的应用就是工具类里用来定义SimpleDateFormat对象,因为这个对象本身不是线程安全的,容易引起数据串扰或是抛出异常。

    内存泄露问题

    Map中的key值是ThreadLocal变量的弱引用,仅仅针对key,此时ThreadLocal实例没有强引用指向,将会gc被回收,但是其在Map中的value不会被回收,直到线程结束整体被gc回收,这就存在了内存泄露的问题,尤其是在线程池中,线程不会被回收而是复用。

    为了避免这个问题,ThreadLocal在每次执行get(),set()时会将key为null的value回收,但是在线程池的环境中仍会出现问题,对此有解决方案:在每次使用完一个ThreadLocal变量后,执行remove()方法移除变量,使其值为null从而被gc回收,或是直接将其定义为private static。


其他并发常见问题

    有效规避死锁

    死锁不只存在于数据库中,两个线程相互等待彼此的资源是放也会产生死锁。

    死锁的产生条件:

  1. 互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放 

  2. 请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。 

  3. 不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用 

  4. 循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。


相关链接:线程间通信知识点补充

相关书籍:《Java并发编程的艺术


多线程应用提高(I) 多线程常见问题、常用方法和关键字2019-12-07鱼鱼

{{commentTitle}}

评论   ctrl+Enter 发送评论