java线程浅谈-对多线程的了解


前言

对所有语言的开发者来说,项目运营中不乏提出的优化,提速需求。在java开发中,高并发、批量数据处理等场景中,结合线程池的设计实现多数情况都是比较优秀的。掌握线程相关知识,在面对相应的场景,从设计到实现会得心应手。


一、线程简述

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
-----------------------------------------------------------------------------------摘自百度百科

1. 线程中的信息

线程运行的过程会产生很多信息,这些信息都保存在Thread类中的成员变量里面,常见的有

  • 线程的ID:getId(),唯一标识
  • 线程的名称:getName(),如果不设置线程名称默认为“Thread-xx”
  • 线程的优先级:getPriority,线程优先级从1-10,其中数字越大表示优先级别越高,同时获得JVM调度执行的可能性越大。一般不推荐设置线程的优先级,如果进行设置了非法的优先级程序就会出现IllegalArgumentException异常。JDK内置了三种常见的状态
MIN_PRIORITY = 1;
NORM_PRIORITY = 5;
MAX_PRIORITY = 10;

2. 自定义线程的方法

实际开发时使用线程,多数先自定义线程对象,基于当前主流JDK版本(1.8),可用的方式有已下三种

  • 继承Thread类
public class Demo extends Thread {
	// 把自定义线程的任务代码写在run方法中。
	@Override 
	public void run() {
		for(int i = 0;i < 100;i ++) {
			System.out.println("自定义线程:" + i);
		}
	}
	public static void main(String[] args) {
		// 创建自定义的线程对象
		Demo d = new Demo(); 
		// 调用start方法启动线程
		d.start(); 
		for(int i = 0;i < 100;i ++) {
			System.out.println("主线程:" + i);
		}
	}
}

  • 实现Runnable接口
// 自定义一个类实现Runnable接口
public class Demo implements Runnable { 
	//实现Runnable接口的run方法,把自定义线程的任务代码写在run方法上
	@Override
	public void run() {
		System.out.println("当前线程:" + Thread.currentThread().getName()); 
		for(int i = 0;i < 100;i ++){
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
	
	public static void main(String[] args) {
		//创建Runnable实现类的对象
		Demo d = new Demo();
		// 4、创建Thread类的对象,并且把Runnable实现类的对象作为实参传递
		 // 创建了一个线程对象,并将其名字赋初值。
		Thread thread = new Thread(d, "测试");
		// 5、调用Thread对象的start方法开启一个线程
		thread.start();
		for(int i = 0;i < 100;i ++) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}
  • 实现 Callable接口
public class Demo implements Callable<String>{
    @Override
    public String call() throws Exception {
        //do something
        return null;
    }
}

3. 线程的状态

关于线程的状态不得不提线程和CPU调度的概率。可以想象一架钢琴,每一个线程都是琴键,服务器是几核(单核单元组,双核双单元组)CPU,就相当于你使用几根手指,调度过程就相当于你用这几根手指去演奏。如果线程优先级、调度规则设定好了,就相当于按乐谱演奏,没有的就相当于即兴演奏。如果有乐谱,那么当你坐在钢琴前准备好时,所有乐谱上的线程是初始状态;开始演奏,乐谱上的线程进入可运行状态;按下按键,这个线程进入执行中状态,不同按键发出声音就是不同线程自身去执行的不同业务;松下按键,这个线程执行完毕。

摘自

  • NEW
    初始状态。新创建的一个线程对象,此时线程对象就是处于初始状态
  • RUNNABLE
  • 可运行状态。通俗的概念就是此时线程获得了CPU的使用权,但是还没轮到它使用,java里线程进入此状态一般有如下方式:
  1. 调用线程的start()方法,此线程进入可运行状态;
  2. 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入可运行状态;
  3. 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入可运行状态;
  4. 锁池里的线程拿到对象锁后,进入可运行状态.
  • RUNNING(一般与可运行状态并为运行状态)
    运行状态中状态。可运行状态的线程获得了cpu 时间片(timeslice) ,执行程序代码。

  • BLOCKED
    阻塞状态。所谓阻塞状态是指正在运行的线程没有运行结束,暂时让出CPU,让其他处于就绪状态的线程就可以获得CPU时间片,进入运行状态。java里线程进入此状态一般有如下方式:

  1. 线程通过调用sleep方法进入睡眠状态;
  2. 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
  3. 线程试图得到一个锁,而该锁正被其他线程持有;
  4. 线程在等待某个触发条件;
  • WAITING
    等待状态。处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。需要注意的是调用obj的wait(), notify()方法前,必须获得obj锁。java里线程进入此状态一般有如下方式:
  1. 当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒;
  2. thread.join(),当前线程里调用其它线程t的join方法,当前线程进入WAITING,当前线程不会释放已经持有的对象锁。
  • TIMED_WAITING(一般与WAITING并作等待状态)
    超时等待。处于这种状态的线程不会被分配CPU执行时间,区别于等待状态的是无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。java里线程进入此状态一般有如下方式:
  1. 当前线程调用对象的wait(long timeout) 方法,wait(long timeout) timeout时间到自动唤醒;
  2. Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态
  • TERMINATED
    执行完毕。当线程的run()方法完成时,或者主线程的main()方法完成时,就判断它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false。java里线程进入此状态一般有如下方式:
  1. 正常执行结束;
  2. 执行过程中出现一个为捕获的异常而结束

4. 实现线程同步的方法

程序允许多线程并发执行,这种场景下,线程间共享资源的操作将会容易引起预期之外的事。比如多个线程一个共享的变量,一个线程去修改了这个变量的值,但是还未修改完成,CPU的调度权被其他线程获取了,其他线程去引用了这个变量,此时其他线程引用的变量值还是修改之前的。为了解决这个问题,就需要同步,保证变量未修改完成前,不会被其他线程引用。java去实现线程同步一般有如下方法:

  • 同步方法
public synchronized void fun(){
        //do something
    }

使用synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。(一点建议:能用同步方法,最好优先考虑使用同步代码块,毕竟方法内不是所有内容都需要同步。同步锁就高速像前方道路变窄,多条道并为一条道,高速上车辆数就是并发量。车少时影响不太大,多了就堵车了。有些时候必须要用,那么就尽可能使这条窄路短些。)

  • 同步代码块
  public void fun(){
        //do something
        synchronized (this){
            //do something
        }
        //do something
    }

即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。当线程开始执行同步代码块前,必须先获得对同步代码块的锁定。并且任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
其中的锁,在非静态方法中可为this,在静态方法中为当前类本身。

  • 使用特殊域变量(volatile)

volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。(常见使用场景:定义某些业务常量,比如业务开关,枚举,可以通过外部工具修改volatile修饰的常量来实现不用重启应用的情况下修改常量值)

  • 使用重入锁(jdk1.5版本后)

ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。要使用时注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。

   private Lock lock = new ReentrantLock();
    public void fun(){
        //do something
        lock.lock();
        try {
            //do something
        }finally {
            lock.unlock();
        }
    }
  • 使用ThreadLocal管理(针对变量,无锁)
   private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue(){
            return 1;
        }
    };
    public void addNum(int num){
        count.set(count.get()+num);
    }
    public int getCount(){
        return count.get();
    }

使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本, 副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。ThreadLocal与同步锁机制对比异同:
同:ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
异:ThreadLocal以"空间换时间",同步锁机制以"时间换空间"。

  • 使用阻塞队列实现(jdk1.5版本后)

java.util.concurrent包提供了多种阻塞队列。利用队列是先进先出的顺序(FIFO),利用这一特性,移除的是队列头元素,添加的元素排在队列尾来隔离不同线程对队列中的变量的操作。 例如LinkedBlockingQueue,常用的API有:

  1. put(E e) : 在队尾添加一个元素,如果队列满则阻塞 ;
  2. size() : 返回队列中的元素个数 ;
  3. take() : 移除并返回队头元素,如果队列空则阻塞 ;
  • 使用原子变量

结合事物的概念,不难发现使用线程同步的根本原因在于对普通变量的操作不具备原子性。在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。例如AtomicInteger,常用的API有:

  1. AtomicInteger(int initialValue) : 创建具有给定初始值的新AtomicInteger
  2. addAddGet(int dalta) : 以原子方式将给定值与当前值相加
  3. get() : 获取当前值
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值