JAVA基础-多线程

Posted 不愿透露姓名的王建森

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA基础-多线程相关的知识,希望对你有一定的参考价值。

1.并行和并发

并行:多个CPU或多个电脑同时处理一段程序

并发:一个CPU或者一个机器,通过CPU调度的方法,让客户看上去同时去执行,实际上从CPU操作层面并不是真正的同时。并发往往需要公共的资源,对公共资源的处理和线程之间的协调是并发的难点。

2.线程基本概念

进程就是程序,有独立的运行内存空间,比如应用和服务,window是支持多进程的系统。

在java中进程就是一个独立的运行在JVM上的程序。

线程:一个程序里面有多个任务,每个任务就是一个线程,线程是共享内存的。

线程包括5种状态:

  1. 新建(New):线程对象被创建时,它只会短暂地处于这种状态.此时它已经分配了必须的系统资源,并执行了初始化。例如,Thread thread = new Thread()。

  2. 就绪(Runnable):称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如, thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。

  3. 运行(Running):线程获取CPU权限进行执行。注意:线程只能从就绪状态进入运行状态。

  4. 阻塞(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞 的情况分为三种:

    • 等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。
    • 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态。
    • 其他阻塞:通过调用线程的sleep()或发出了I/O请求时,线程会进 入到阻塞状态。当sleep()状态超时、join()等待线程终止或是超时。或是I/O处理完毕时, 线程重新转入就绪状态。
  5. 死亡(Dead):线程执行完了或者 因异常退出了run()方法,该线程结束生命周期

3.实现多线程的两种方式

我们可以通过继承JAVA中Thread类和实现Runnable接口这两种方式来实现多线程。

继承Thread:

public class TicketThread extends Thread{
/**
 * 方式一:直接继承Thread
 * @param args
 */
	private int tickets = 10;
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i=0;i<5;i++) {
			if(tickets > 0)
				System.out.println(this.getName()+"买票,剩余" + tickets--);
			else
				System.out.println("票买完了");
		}
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub

		TicketThread t1 = new TicketThread();
		TicketThread t2 = new TicketThread();
		TicketThread t3 = new TicketThread();
		t1.start();
		t2.start();
		t3.start();
	}

}

/**
Thread-0买票,剩余10
Thread-0买票,剩余9
Thread-2买票,剩余10
Thread-1买票,剩余10
Thread-2买票,剩余9
Thread-0买票,剩余8
Thread-2买票,剩余8
Thread-1买票,剩余9
Thread-2买票,剩余7
Thread-0买票,剩余7
Thread-2买票,剩余6
Thread-1买票,剩余8
Thread-0买票,剩余6
Thread-1买票,剩余7
Thread-1买票,剩余6
/

实现Runnable接口:

public class TicketThread2 implements Runnable {
/**
 * 方法二,实现Runnable接口
 * @param args
 */
	
	private int tickets = 10;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		// TODO Auto-generated method stub
		for(int i=0;i<5;i++) {
			if(tickets > 0)
				System.out.println(Thread.currentThread().getName()+"买票,剩余" + tickets--);
			else
				System.out.println("票买完了");
		}
	}
	
	
	public static void main(String[] args) {
		//任务类
		TicketThread2 task1 = new TicketThread2();
		//以任务类,创建线程
		Thread t1 = new Thread( task1 );
		Thread t2 = new Thread( task1 );
		Thread t3 = new Thread( task1 );
		t1.start();
		t2.start();
		t3.start();
	}
}

/**
Thread-0买票,剩余9
Thread-1买票,剩余10
Thread-2买票,剩余8
Thread-1买票,剩余6
Thread-0买票,剩余7
Thread-1买票,剩余4
Thread-1买票,剩余2
Thread-2买票,剩余5
票买完了
票买完了
票买完了
Thread-1买票,剩余1
Thread-0买票,剩余3
票买完了
票买完了
/

因为JAVA是单继承的,所以实际使用的时候我们使用Runnable接口比较多。

另外我们可以设置优先级:

常量:MAX_PRIORITY 最高优先级,值为10

常量:MIN_PRIORITY 最低优先级,值为1

常量:NORM_PRIORITY 默认优先级,值为5

比如:

	public static void main(String[] args) {
		//任务类
		TicketThread2 task1 = new TicketThread2();
		//以任务类,创建线程
		Thread t1 = new Thread( task1 );
		Thread t2 = new Thread( task1 );
		Thread t3 = new Thread( task1 );
		t3.setPriority(Thread.MAX_PRIORITY);
		t1.setPriority(Thread.MIN_PRIORITY);
		t3.start();
		t2.start();
		t1.start();
	}

4.锁的概念和使用

由于线程之间可以共享内存,则某个对象(变量)是可以被多个线程共享的,是可以被多个线程同时访问的。当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何 交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么 就称这个类是线程安全的。

在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:竞态条件(Race Condition)。

竟态条件会使运行结果变得不可靠,程序的运行结果取决于方法的调用顺序,将方法以串行的方式来访问,我们称这种方式为同步锁(synchronized)。

Java实现同步锁的方式有:

  • 同步方法synchronized method
  • 同步代码块 synchronized(Lock) -等待与唤醒 wait 和 notify
  • 使用特殊域变量(volatile)实现线程同步
  • 使用重入锁实现线程同步ReentrantLock
  • 使用局部变量实现线程同步ThreadLocal

synchronized

synchronized 是JAVA的内置锁机制,实在JVM上的

//同步代码块
synchronized (object){
    
}
//同步方法
public synchronized void method(){
    //do something。
}

注意:同步是一种高开销的操作,因此应该尽量减少同步的内容,通常没有必要同步整个方法。

Reentrant Lock

可重入锁,是一种显示锁,在JAVASE5.0中新增加了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法具有相同的基本行为和语义,并且扩展了其能力。

可重入:甲获得锁并释放后,乙可以继续获得锁。

//创建一个synchronized实例
ReentrantLock();
//获得锁
lock();
//释放锁
unlock();

5.生产消费模型

生产者消费者模型,是一个非常典型的多线程并发处理的模型,在实际的生产应用中也有非常广泛的使用。

下面我们用这个模型,来演示上面的两种多线程方法:

这是一个多线程买票卖票的例子

  • synchronized代码块来实现,synchronized代码块实现多线程主要是利用.wait()函数和.notify()函数。

    先构建一个store代表售票处:这里使用synchronized来对票的买入和卖出做多线程的管理。

import java.util.LinkedList;

public class Store {
/**
 * 存储-队列实现
 */
	//创建队列
	LinkedList<Integer> list = new LinkedList<>();
	//最大值
	int max = 10;
	
	public void push(int n) {
		try {
			//同步对列
			synchronized (list) {
				if(list.size() >= max) {
					System.out.println("队列满了");
					//线程挂起
					list.wait();
				//队列不满,可以放入(有可能为空)
				}else {
					System.out.println("放入"+n);
					list.add(n);
					//唤醒取的线程
					list.notify();
				}
			}
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
	//从队头弹出
	public int pop() {
		try {
			//同步对列
			synchronized (list) {
				if(list.size() == 0) {
					System.out.println("队列空");
					//线程挂起
					list.wait();
				}else {
					int n = list.poll();
					System.out.println( "取出"+n );
					//唤醒放入的线程
					list.notify();
					return n;
				}
			}
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		return -1;
	}
}

随后我们构建生产者和消费者:

生产者和消费者实现了Runnable接口,并把买入和卖出写在了run函数里面,这样我们在创建多线程的时候,就会调用run函数。

public class CustomerTask implements Runnable{
/**
 * 消费者线程
 */
	private Store store;
	public CustomerTask( Store store) {
		// TODO Auto-generated constructor stub
		this.store = store;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		try {
			//休息200毫秒
			Thread.sleep(200);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//从仓库中取出
		System.out.println("消费"+store.pop());
		
	}

	
}

import java.awt.TexturePaint;

public class ProductorTask implements Runnable{
/**
 * 生产者线程
 * @param args
 */
	private Store store;
	public ProductorTask( Store store) {
		// TODO Auto-generated constructor stub
		this.store = store;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
			//模拟0-100随机数的产生
			int n = (int)((Math.random())*100);
			//将随机数放入仓库中
			store.push(n);
	}

}

最后我们在主函数中,构建买入和卖出的逻辑关系:

public class RunPCM {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		Store store = new Store();
		
		//创建生产者线程
		Thread t1 = new Thread(  new ProductorTask(store) );
		
		//创建消费者线程
		Thread t2 = new Thread(new CustomerTask(store));
		
		t1.start();
		t2.start();
	}

}

以上就是使用synchronized代码块的例子。

  • 下面来实现一个ReentrantLock的例子:

    ReentrantLock需要构建一个对象监视器,ReentrantLock是利用对象监视器中的方法来实现唤醒其他线程的:condition.await();和condition.signal();我们对于上一个例子仅需要做微小的改动。

    package test2;
    import java.util.LinkedList;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Store {
    /**
     * 存储-可重入锁实现
     */
    	//创建队列
    	LinkedList<Integer> list = new LinkedList<>();
    	//最大值
    	int max = 10;
    	//创建可重入锁
    	Lock lock = new ReentrantLock();
    	//对象监视器
    	Condition condition = lock.newCondition();
    	
    	public void push(int n) {
    		try {
    			//获得锁
    			lock.lock();
    			if(list.size() >= max) {
    				System.out.println("队列满了");
    				//线程挂起
    				condition.await();
    			//队列不满,可以放入(有可能为空)
    			}else {
    				System.out.println("放入"+n);
    				list.add(n);
    				//唤醒取的线程(信号量)
    				condition.signal();
    			}
    		} catch (Exception e) {
    			// TODO: handle exception
    			e.printStackTrace();
    		}finally {
    			//释放锁
    			lock.unlock();
    		}
    	}
    	//从队头弹出
    	public int pop() {
    		try {
    			//同步对列
    			lock.lock();
    			if(list.size() == 0) {
    				System.out.println("队列空");
    				//线程挂起
    				condition.await();
    			}else {
    				int n = list.poll();
    				System.out.println( "取出"+n );
    				//唤醒放入的线程
    				condition.signal();
    				return n;
    			}
    		} catch (Exception e) {
    			// TODO: handle exception
    			e.printStackTrace();
    		}finally {
    			lock.unlock();
    		}
    		return -1;
    	}
    
    
    }
    
    

    然后是生产者和消费者(这个部分我们都不需要改动)

    package test2;
    
    public class CustomerTask implements Runnable{
    /**
     * 消费者线程
     */
    	private Store store;
    	public CustomerTask( Store store) {
    		// TODO Auto-generated constructor stub
    		this.store = store;
    	}
    	@Override
    	public void run() {
    		// TODO Auto-generated method stub
    		try {
    			//休息200毫秒
    			Thread.sleep(200);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		//从仓库中取出
    		System.out.println("消费"+store.pop());
    	}
    }
    
    
    package test2;
    import java.awt.TexturePaint;
    
    public class ProductorTask implements Runnable{
    /**
     * 生产者线程
     * @param args
     */
    	private Store store;
    	public ProductorTask( Store store) {
    		// TODO Auto-generated constructor stub
    		this.store = store;
    	}
    
    	@Override
    	public void run() {
    		// TODO Auto-generated method stub
    			//模拟0-100随机数的产生
    			int n = (int)((Math.random())*100);
    			//将随机数放入仓库中
    			store.push(n);
    	}
    }
    
    

    最后是主函数的逻辑(我们也不需要改动):

    package test2;
    
    public class RunPCM {
    
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		
    		Store store = new Store();
    		
    		//创建生产者线程
    		Thread t1 = new Thread(  new ProductorTask(store) );
    		
    		//创建消费者线程
    		Thread t2 = new Thread(new CustomerTask(store));
    		
    		t1.start();
    		t2.start();
    	}
    
    }
    
    

6. volatile关键字

Java 内存模型中的可见性、原子性和有序性。

可见性:当多个线程访问同一个变量x时,线程1修改了变量x的值,线程1、线程2...线程n能够立即读

取到线程1修改后的值。

有序性:即程序执行时按照代码书写的先后顺序执行。在Java内存模型中,允许编译器和处理器对指

令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

原子性:原子性通常指多个操作不存在只执行一部分的情况,要么全部执行、要么全部不执行。

volatile具有可见性、有序性,不具备原子性。

  • synchronized是阻塞式同步,称为重量级锁。

  • volatile是非阻塞式同步,称为轻量级锁。

被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。

7.线程池的概念和使用

线程的创建是比较消耗内存的,所以我们要事先创建若干个可执行的线程放进一个“池(容器)” 里面,需要的时候就直接从池里面取出来不需要自己创建,使用完毕也不需要销毁而是放进“池”中, 从而减少了创建和销毁对象所产生的开销。

  • ExecutorService:线程池接口

  • ExecutorService pool(池名称) = Executors.常用线程池名;

常用线程池

  • newsingleThreadExecutor :单个线程的线程池,即线程池中每次只有一个线程在工作,单线程 串行执行任务

  • newfixedThreadExecutor(n):固定数量的线程池,每提交一个任务就是一个线程,直到达到线程 池的最大数量,然后在后面等待队列前面的线程执行或者销毁

  • newCacheThreadExecutor:一个可缓存的线程池。当线程池超过了处理任务所需要的线程数,那 么就会回收部分闲置线程(一般是闲置60s)。当有任务来时而线程 不够时,线程池又会创建新的线程,当线程够时就调用池中线程。适 用于大量的耗时较少的线程任务。

  • newScheduleThreadExecutor:一个大小无限的线程池,该线程池多用于执行延迟任务或者固定周 期的任务。

    下面我们利用线程池来实现上述例子。

package pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TicketPool {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		/**
		 * 利用线程池创建线程
		 */
		
		//创建放5个线程的线程池
		ExecutorService service = Executors.newFixedThreadPool(5);
		for(int i = 0; i < 5;i++) {
			//执行线程;
			service.execute(new TicketThread());
		}
		//关闭线程池
		service.shutdown();	
	}

}
/*
pool-1-thread-1买票,剩余10
pool-1-thread-3买票,剩余8
pool-1-thread-2买票,剩余9
pool-1-thread-4买票,剩余5
pool-1-thread-3买票,剩余6
pool-1-thread-1买票,剩余7
pool-1-thread-3买票,剩余1
pool-1-thread-4买票,剩余2
pool-1-thread-5买票,剩余3
pool-1-thread-2买票,剩余4
pool-1-thread-5票买完了
pool-1-thread-4票买完了
pool-1-thread-3票买完了
pool-1-thread-1票买完了
pool-1-thread-3票买完了
pool-1-thread-4票买完了
pool-1-thread-5票买完了
pool-1-thread-2票买完了
pool-1-thread-5票买完了
pool-1-thread-4票买完了
pool-1-thread-1票买完了
pool-1-thread-5票买完了
pool-1-thread-2票买完了
pool-1-thread-2票买完了
pool-1-thread-1票买完了

*/
package pool;

public class TicketThread extends Thread{
/**
 * 方式一:直接继承Thread
 * @param args
 */
	private static int tickets = 10;
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i=0;i<5;i++) {
			if(TicketThread.tickets > 0)
				System.out.println(Thread.currentThread().getName()+"买票,剩余" + TicketThread.tickets--);
			else
				System.out.println(Thread.currentThread().getName()+"票买完了");
		}
	}
}

以上是关于JAVA基础-多线程的主要内容,如果未能解决你的问题,请参考以下文章

java基础入门-多线程同步浅析-以银行转账为样例

java多线程基础

Java基础之多线程

Java多线程基础

多线程编程学习一(Java多线程的基础)

Java 多线程基础多线程的实现方式