JAVA多线程进阶

目录

线程的同步方式(也叫同步锁)

  • 方式一: synchronized 代码块
synchronized(资源对象){// 实现锁的对象资源
	// 需要统一执行的原子资源
	// 这段内容是不可分割的
}
  1. synchronized后面的资源对象,最好是线程需要竞争的唯一的
  2. 内部代码块中执行的内容是统一整体当结束代码块时候锁被释放掉
public class TestSynchronized{
	public static void main(Stirng[] args){
		ThreadOne one = new ThreadOne();
		Thread th = new Thread(one);
		Thread th1 = new Thread(one); 
		
		th.start();
		th.start();
	}
}
// 资源
class ThreadOne implements Runnable{
	public void run(){
	// 同步代码块上锁
	synchronized(this){
		for(int i=1;i < 100;i++){
	System.out.print(Thread.currentThread().getName()+":"+i);
		}
	}
	}
}
  • 方式二: 同步方法
 synchronized 返回值类型 方法名称(形参列表0){// 当前对象(this)加锁
}
  1. 只有拥有对象互斥锁标记的线程,才能进入该对象的同步方法中

  2. 线程退出同步方法时,会释放相应的互斥锁标记

  3. 已知的java jdk内库中线程安全类有:StringBuffer、Vector、Hashtable、这几个类中的公开方法,均为synchronized修饰的方法

public class TestSynchronized{
	public static void main(Stirng[] args){
		ThreadOne one = new ThreadOne();
		Thread th = new Thread(one);
		Thread th1 = new Thread(one); 
		
		th.start();
		th.start();
	}
}
// 资源
class ThreadOne implements Runnable{
	public  void run(){
	
		start();
	}
	// 方法的同步修饰
	public synchronized void start(){
		for(int i=1;i < 100;i++){
			System.out.print(Thread.currentThread().getName()+":"+i);
			}
	}
}

同步规则

  • 只有调用同步代码块的方法,或者同步方法时,才需要对象的锁标记
  • 如调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用

加锁的场景
- 写(增、删、改)操作时候加锁
- 读操作时候,不加锁

死锁

  • 当第一个线程拥有对象A的锁标记,并且等待B对象的锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁问题。
  • 一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有饿锁标记,由此可能造成死锁问题。

常见的死锁具体问题

  • 生产者与消费者问题
    若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区取走产品,也不允许生产者向一个满的缓冲区中放入产品

线程通信

线程通信时解决思索地有效方法

  • 等待
    • public final void wait()
    • public final void wait(long timeout)
    • 必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。同时此线程阻塞在o的等待队列中。释放锁,进入等待队列。
  • 通知
    • public finall void notify()
    • public final void notifyAll()
    • 必须在对obj加锁的同步代码块中。从obj的Waiting中释放一个或全部线程。对自身没有任何影响

以下是个简单的案例
也就是李敢敢(丈夫)与赵岩岩(妻子) 银行卡子母卡同时取钱的案例

public class TestWaitNotify {
	public static void main(String[] args) {
		//临界资源,被共享的对象
		//临界资源对象只有一把锁
		Account acc =new Account("6002","1234",2000);
		//两个线程对象 共享同一银行卡资源对象。
		//给定任务后,第二个参数是对线程自定义命名
		Thread husband = new Thread(new Husband(acc),"丈夫");
		Thread wife = new Thread(new Wife(acc),"妻子");
		// 启动线程
		husband.start();
		wife.start();
	}
}
class A extends Thread{
	
}
// 模拟现实世界的子母卡
class Husband implements Runnable{
	Account acc;
	public Husband(Account acc) {
		this.acc = acc;
	}
	public void run() {
		this.acc.withdrawal("6002","1234",1200);//整体式原子操作
	}
}
class Wife implements Runnable{
	Account acc;
	public Wife(Account acc) {
		this.acc = acc;
	}
	public void run() {
		this.acc.withdrawal("6002","1234",1200);//整体式原子操作
	}
}
class Account{
	String cardNO;// 卡号
	String password;// 密码
	double balance; // 余额
	public Account(String cardNO, String password, double balance) {
		super();
		this.cardNO = cardNO;
		this.password = password;
		this.balance = balance;
	}
	// 取款操作(整体是个原子操作,从插卡开始验证,到取款成功的一系列步骤,不可以缺少或者打乱)
	public synchronized void withdrawal(String no,String pwd,double money) {
		// 等待! --->阻塞状态
		System.out.println(Thread.currentThread().getName()+"正在读卡...");
		if(no.equals(this.cardNO) && pwd.equals(this.password)) {
			System.out.println(Thread.currentThread().getName()+"验证成功...");
			if(money <=  this.balance) {
				try {
					Thread.sleep(1000);//模拟现实世界ATM机器查钱
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				this.balance = this.balance - money;
				System.out.println(Thread.currentThread().getName()+"取款成功!当前余额为:"+this.balance);
			}else {
				System.out.println(Thread.currentThread().getName()+"当前卡内余额不足!");
			}
		}else {
			System.out.println(Thread.currentThread().getName()+"卡号或密码错误!");
		}
	}
}

高级多线程

线程池的原理
  • 现有问题:
    • 线程是宝贵的内存资源、单个线程约占1MB空间,过多分配易造成内存溢出
    • 频繁的创建及销毁线程会增加虚拟机的回收频率、资源开销、造成程序性能下降

所以就引入线程池来解决这个问题

  • 线程池:
    • 线程容器,可设定线程分配的数量上限
    • 将预先创建的线程对象存入池中,并重用线程池中的线程对象
    • 避免频繁的创建和销毁

将任务提交给线程池,有线程池分配线程、运行任务,并在当前任务结束后服用线程

  • java.util.concurrent 所有的线程池类的祖先

并发编程中很常用的实用工具类

  • Executor: 线程池的顶级接口
  • ExecutorService:线程池接口(常用的更丰富也是个接口),可通过submit()(提交任务代码)提交一个Runnable任务用于执行,并返回一个标识该任务的Future(结果)
  • Executors工厂类:通过此类可以获得一个线程池
  • 通过static ExecutorService new FixedThreadPool(int nThreads) 获取固定数量的线程池。参数:线程池中的线程的数量
  • 通过newCachedThreadPool() 获得动态数量的线程池,如果不够则创建新的,没有上限
public class TestThreadPool {
	public static void main(String[] args) {
	// 利用线程池的工厂方法来生产对象 参数3代表创建三个线程
		ExecutorService es = Executors.newFixedThreadPool(3) ;//手动限定线程池里的线程数量。
		MyTask a = new MyTask();
		//2.将任务提交到线程池,由线程池调度、执行
		es.submit(a);
		es.submit(a);
		es.submit(a);
		es.submit(a);
		es.submit(a);
		es.submit(a);
	}
}

// 线程任务
class MyTask implements Runnable{
	public void run() {
		for(int i = 1;i<=50;i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
}
public class TestThreadPool {
	public static void main(String[] args) {
	// 利用线程池的工厂方法来生产对象 参数3代表创建三个线程
		ExecutorService es = Executors.newCachedThreadPool();//自动扩充线程池中线程数量
		MyTask a = new MyTask();
		//2.将任务提交到线程池,由线程池调度、执行
		es.submit(a);
		es.submit(a);
		es.submit(a);
		es.submit(a);
		es.submit(a);
		es.submit(a);
	}
}

// 线程任务
class MyTask implements Runnable{
	public void run() {
		for(int i = 1;i<=50;i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
}

newCachedThreadPool() 与 newFixedThreadPool(3) 的对比

  • newCachedThreadPool()是动态创建线程,不用对程序进行线程创建数量的预估,而且用户体检感较好,但是最大的诟病是线程足够多时会内存溢出
  • newFixedThreadPool()给与评估过后的线程数量,可能会在用户过多时候慢些,也可通过维护服务器时修改线程数量等方法增加扩充。
接口Callable
  • 类似于Runnable,两者都是为了哪些实例可能被另一个线程执行的类设计的,但是Runnable没有返回结果
public interface Callable<V>{public  V 	call() throws Exception;

}
  • JDK5加入的,与Runnable接口类似,实现之后代表一个线程任务
  • Callable具有泛型返回值、可以声明异常
public class TestCallable {
	public static void main(String[] args) {
		// 创建一个线程池
		ExecutorService es = Executors.newFixedThreadPool(3);
		
		MyTask1 task = new MyTask1();
		// 将任务放入线程池
		es.submit(task);
	}
}

class MyTask1 implements Callable<Integer>{
	
	public Integer call() throws Exception{
		
		for (int i = 0; i < 100; i++) {
			if(i == 30) {
				Thread.sleep((int)(Math.random()*1000));
			}
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
		return null;
	}
}

Future接口
  • 异步接收ExecutorService.submit()所返回的状态结果,当中包含了call()返回值
  • 方法:V get()以阻塞形式等待Future中的异步处理结果(call()的返回值)

示例:两个线程,并发计算1 ~ 50、51 ~ 100的和,在进行汇总统计

public class TestFuture {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		ExecutorService es = Executors.newFixedThreadPool(3);
		
		MyCall mc = new MyCall();
		MyCall2 mc2 = new MyCall2();
		//通过submit执行提交的任务,Future接受返回的结果
		Future<Integer> result = es.submit(mc);
		Future<Integer> result1 = es.submit(mc2);
		//通过Future的get方法,获得线程执行完毕后的结果.
		Integer value =  result.get();
		Integer value2 = result1.get();
		System.out.println(value + value2);
	}
}

//计算1~50的和
class MyCall implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		Integer sum = 0;
		for(int i = 1;i<=50;i++) {
			sum = sum + i;
		}
		return sum;
	}
	
}
//计算51~100的和
class MyCall2 implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		Integer sum = 0;
		for(int i =51;i<=100;i++) {
			sum = sum + i;
		}
		return sum;
	}
}
扩充
  • 同步
    • 形容一次方法调用,同步一旦开始,调用者必须等待该方法返回,才能继续。
    • 单条执行路径
  • 异步
    • 形容一次方法调用,异步一旦开始,像是一次消息传递,调用者告知之后立刻返回。二者竞争时间片,并发执行
发布了31 篇原创文章 · 获赞 33 · 访问量 5960

猜你喜欢

转载自blog.csdn.net/lxn1214/article/details/104825991