zoukankan      html  css  js  c++  java
  • Java多线程

    Java多线程

    线程与进程的区别

    • 进程:是程序的依次执行过程,正在运行的程序,存在生命周期。进程为资源分配的单位。每个进程在内存中有独占一个方法区和堆空间,被多个线程共享。
    • 线程:进程可以进一步细化为线程,是程序内部的一条执行路径。线程作为调度和执行的单位。每个线程拥有独立的虚拟栈空间和程序计数器。

    线程的创建和使用

    线程的创建
    • 方式一:创建继承Thread类的子类,需要重写父类的run()方法,然后创建子类的对象,通过子类对象调用start()方法(包括采用匿名子类)。
    public class MultiThreadingTest1 {
    	public static void main(String[] args) {
    		MyThread1 myThread = new MyThread1();
    		myThread.start();
    		//如果直接调用run方法不属于多线程,因为没有开启新线程
    		//myThread.run();
    		//同一个新线程的对象不能重复start启动,如想重新启动新线程,需要创建一个新的对象
    		//myThread.start();//运行报错:java.lang.IllegalThreadStateException
    		System.out.println("我是主线程");
    		
    		//匿名子类写法创建多线程
    		new Thread() {
    			@Override
    			public void run() {
    				// 此线程执行需要执行的操作声明在run中
    				int sum = 0;
    				for (int i = 0; i <= 100; i++) {
    					sum += i;
    				}
    				System.out.println("我是匿名子类新线程");
    			}
    		}.start();
        }
    }
    class MyThread1 extends Thread {
    	@Override
    	public void run() {
    		// 此线程执行需要执行的操作声明在run中
    		int sum = 0;
    		for (int i = 0; i <= 100; i++) {
    			sum += i;
    		}
    		System.out.println("我是新线程");
    	}
    }
    
    
    • 方式二:创建实现Runnable的类,实现Runnable中的抽象方法run(),创建类的对象,将此对象传入Thread类的构造器中创建Thread类的对象,调用Thread类的对象的start()方法(包括匿名写法)。

       public class MultiTheadingTest2 {
      	public static void main(String[] args) {
      		MyThread2 myThread2 = new MyThread2();
      		Thread thread = new Thread(myThread2);
      		thread.start();
      		System.out.println("我是主线程");
      		//匿名写法
      		new Thread(
      				new Runnable() {
      					@Override
      					public void run() {
      						// 此线程执行需要执行的操作声明在run中
      						int sum = 0;
      						for (int i = 0; i <= 100; i++) {
      							sum += i;
      						}
      						System.out.println("我是匿名新线程");
      					}
      			}).start();
      }
      }
      class MyThread2 implements Runnable{
      	@Override
      	public void run() {
      		// 此线程执行需要执行的操作声明在run中
      				int sum = 0;
      				for (int i = 0; i <= 100; i++) {
      					sum += i;
      				}
      				System.out.println("我是新线程");
      	}	
      }
      
    • 方式三:JDK5.0新特性。实现Callable接口。需要借助Future接口的唯一实现类FutureTask辅助线程的对象创建和返回值获取(FutureTask还实现了Runnable接口),再创建Thread对象,将FutureTask类的对象作为构造器参数传入,完成线程的创建,最后调用start()方法完成线程启动。

      import java.util.concurrent.Callable;
      import java.util.concurrent.ExecutionException;
      import java.util.concurrent.FutureTask;
      
      public class MultiThreadTest3 {
      	public static void main(String[] args) {
      		MyThread3 myThread3 = new MyThread3();
      		FutureTask futureTask = new FutureTask(myThread3);
      		Thread thread = new Thread(futureTask);
      		thread.start();
      		
      		try {
      			Object sum = futureTask.get();
      			//System.out.println(sum);
      		} catch (InterruptedException e) {
      			e.printStackTrace();
      		} catch (ExecutionException e) {
      			e.printStackTrace();
      		}
      		System.out.println("我是主线程");
      
      		// 匿名写法
      		new Thread(new FutureTask(new Callable() {
      
      			@Override
      			public Object call() throws Exception {
      				// 此线程执行需要执行的操作声明在call中
      				int sum = 0;
      				for (int i = 0; i <= 100; i++) {
      					sum += i;
      				}
      				System.out.println("我是匿名新线程");
      				return sum;// int类型赋值给Object,自动装箱
      			}
      		}) ).start();
      	}
      }
      
      class MyThread3 implements Callable {
      
      	@Override
      	public Object call() throws Exception {
      		// 此线程执行需要执行的操作声明在call中
      		int sum = 0;
      		for (int i = 0; i <= 100; i++) {
      			sum += i;
      		}
      		System.out.println("我是新线程");
      		return sum;// int类型赋值给Object,自动装箱
      	}
      }
      
    • 方式四:JDK5.0新特性。使用线程池,提前创建好多个线程放入线程池中,使用时直接获取,使用完放回线程池中。可以做到提高响应速度(减少线程创建的时间)和降低资源消耗(可重复利用线程)。利用Executors工具类创建线程池,然后提供Runnable(excute())或Callable(submit())接口的实现类的对象,执行指定线程的操作。最后,关闭线程池shutdown()

      import java.util.concurrent.Callable;
      import java.util.concurrent.Executor;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      
      public class MultiThreadTest4 {
      	public static void main(String[] args) {
      		// 利用工具类Executors创建线程池
      		ExecutorService threadPool = Executors.newFixedThreadPool(10);
      		threadPool.submit(new MyThread4());// 适用于实现Callable接口的线程
      		threadPool.execute(new MyThread5());// 适用于实现Runnable接口的线程
      		threadPool.shutdown();
      
      	}
      }
      
      class MyThread4 implements Callable{
      
      	@Override
      	public Object call() throws Exception {
      		// 此线程执行需要执行的操作声明在call中
      		int sum = 0;
      		for (int i = 0; i <= 100; i++) {
      			sum += i;
      		}
      		System.out.println("我是CALL新线程");
      		return sum;
      	}
      	
      }
      
      class MyThread5 implements Runnable {
      
      	@Override
      	public void run() {
      		// 此线程执行需要执行的操作声明在call中
      		int sum = 0;
      		for (int i = 0; i <= 100; i++) {
      			sum += i;
      		}
      		System.out.println("我是RUN新线程");
      	}
      
      }
      
      创建线程方式间的比较

      JDK5.0前两种方式的比较(继承Thread类与实现Runnable接口)

      • 继承方式的弊端:单继承约束下线程只能继承于Thread类,不能继承于其他的类
      • 实现方式的优势:天然存在共享数据的情况,不需要将共享的数据设置为静态
      • Thread类实际上是Runnable接口的实现类

    实现Runnable接口和Callable接口方式的比较

    • Runnable接口实现方法需要重写run(),但是run()方法没有返回值,Callable接口实现方式重写Call()方法,可以有返回值;
    • Runnable接口实现方法不能抛出异常,只能try-catch捕获异常,Callable接口实现方式可以throws抛出异常;
    • Runnable接口实现方法不支持泛型,Callable接口实现方式支持泛型;
    • Callable接口实现方式返回值需要借助FutureTask类获取返回值。
    线程的常用方法
    • 1.start():启动当前线程,自动调用run()方法;
    • 2.run():通常需要子类重写,并将新线程需要执行的操作声明在run()方法中;
    • 3.currentThread():静态方法,返回当前执行的线程的对象(返回对象类型为Thread);
    • 4.getName():获取当前线程的名称,不设置名称情况下,默认调用Thread的空参构造器Thread-0等等(线程名不等于类名);
    • 5.setName():设置当前线程的名称,Thread.currentThread.setName("")或者 对象名.setName("")
    • 6.yield():静态方法,线程让步,释放当前CPU的执行,可以让多个线程同时竞争资源;
    • 7.join():在线程A中调用线程B的join()方法,相当于让线程B直接插入线程A方法中运行(线程A阻塞),直至线程B结束,继续线程A;
    • 8.stop():强制结束线程的生命周期,不推荐使用(Deprecated);
    • 9.sleep(long millis):静态方法,毫秒单位,睡眠一段时间之后重新加入CPU资源竞争,睡眠的时候仍然握锁;
    • 10.isAlive():判断当前线程是否存活。
    线程的优先级
    • MAX_PRIORITY:默认线程最高优先级为10
    • MIN_PRIORITY:默认线程最低优先级为1
    • NORM_PRIORITY:不设置优先级情况下,默认线程优先级为5
    • getPriority()final,获取线程优先级
    • setPriority(int):设置线程优先级
    • 注:高优先级并不意味着先执行,只是更高概率先抢占资源执行,所以还是存在交替输出的结果。
    多线程共享数据
    • 使用方式一Thread子类的方式创建对象,需要将共享数据或者锁等设置为静态。
    • 使用方式二Runnable接口实现类方法创建多线程,可以不用设置静态。
      经典例子:三个窗口卖票,采用方式一创建多线程,需要将票的总数需要设置为静态。存在线程不安全的问题。

    线程的生命周期

    JDK中Thread.State枚举类定义了线程的几种状态:

    • 新建:当一个Thread类或其子类声明并被创建,新生的线程对象处于新建状态;

    • 就绪:新建状态的线程被start()后,进入线程队列等待时间片,但还未分配cpu资源,处于就绪状态;

    • 运行:线程被cpu调度进入运行状态;

    • 阻塞:在某些特殊情况下,被人为挂起或执行输入输出操作时,线程临时终止自己的执行,进入阻塞状态;

    • 死亡:线程完成了所有工作或被提前强制性终止或出现异常导致结束。

    线程安全与线程同步

    线程安全问题产生原因:当一个线程在执行操作共享数据的多条代码过程中,其他线程也参与了运算,就有可能导致线程安全问题的产生。
    解决方案:同步机制。

    同步机制

    方式一:同步代码块
    将共享数据资源的代码块“包”起来,加synchronized关键字和同步锁。

    synchronized(同步监视器(锁)){
    //需要被同步的代码块
    }
    

    说明:

    • 操作共享数据的代码即为需要被同步的代码;
    • 多个线程共同操作的变量即为共享数据;
    • 任何一个类的对象都能作为锁;多个线程必须共用一把锁,比如用方法一创建线程需要设置锁对象为静态
    • 方式二创建线程中,解决线程安全问题,锁可以设置为当前对象,即用关键字this表示,synchronized(this){同步代码块}
    • 方式一创建线程中,解决线程安全问题,锁可以设置为类.class,即synchronized(继承Thread的子类.class){同步代码块};
    • 在操作同步代码块时,只有一个线程参与,其他线程等待,相当于一个单线程的过程,效率低。

    方式二:同步方法

    如果操作共享数据的代码完整声明在一个方法中,不妨将此方法声明为同步的。

    • 方式一创建线程时,可以直接把操作共享数据的代码封装进一个方法里,并把该方法声明为静态**和同步,即staticsynchronized,这里的隐藏同步监视器即为当前类本身,类.class
    • 方式二创建线程时,可以直接把操作共享数据的代码封装进一个方法里,并把该方法声明为synchronized,这里实际上仍然有隐藏同步监视器存在,即为this。必要情况下,可以直接把run()方法设置为synchronized,但是相当于变成一个单进程过程,共享数据会被第一个进程全部执行。

    方式三:同步锁(Lock)

    • JDK5.0新特性,同步锁由Lock对象充当。ReentrantLock类实现Lock接口。
    • java.util.concurrent.locks.Lock接口:控制多个线程对共享数据资源进行访问的工具。锁提供了对共享数据资源的独占访问,每次只有一个线程对Lock对象加锁,线程开始访问共享数据资源之前需要先获取Lock对象。
    • 同样,使用方式一创建线程需要主要lock对象的静态问题
            ReentrantLock reentrantLock = new ReentrantLock();
    		try {
    			//加锁
    			reentrantLock.lock();
    			//需要同步的代码块
    		}finally {
    			//释放锁
    			reentrantLock.unlock();
    		}
    
    两大类解决线程安全问题方法的不同之处(synchronized和lock(ReentrantLock))
    • synchronized机制属于在执行完同步代码后,会自动释放锁(隐式锁);
    • Lock机制需要手动的启动和释放锁(显示锁),且只有代码块锁,没有方法锁;
    单例模式线程安全问题

    单例模式的创建,一般分为两种方式:饿汉式懒汉式

    • 饿汉式:在还没需要使用对象前,提前在开辟内存空间,创建对象;
    • 懒汉式:在需要使用对象的时候,判断是否需要新建对象的时候考虑创建对象;

    懒汉式单例设计模式,存在线程安全的问题,有可能多个线程“同时”判断是否需要创建新对象,导致创建的对象个数多于一个。下面代码是两种创建模式的简单代码示例,并直接利用同步方法解决懒汉式单例模式的线程安全问题。

    public class singletonTest {
    	public static void main(String[] args) {
    		Bank bank1 = Bank.getBankInfo();
    		School school1 = School.getSchoolInfo();
    
    	}
    }
    
    //饿汉式单例模式:没用就直接造好了
     class Bank{
    	 //构造器私有化
    	 private Bank(){ 
    	 } 
    	 //内部创建对象
    	  private static Bank bank = new Bank();
    	  //静态开放方法调用
    	  public static Bank getBankInfo() {
    		  return bank;
    	  }
     }
     
     //懒汉式单例模式:用的时候造,开辟空间
     class School{
    	 private School() {
    	 }
    	 private static School school = null;
    	 public static synchronized School getSchoolInfo() {
    		 if(school == null) {
    			 school = new  School();
    		 }
    		 return school;
    	 }
     }
    
    死锁

    不同的线程分别占用对方需要的同步资源(锁)不放弃,都在等待对方放弃同步资源,形成线程的死锁。出现死锁后,不会出现异常,不会有提示信息,只是所有的线程都处于阻塞状态,无法继续。若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

    public class ThreadDeadlock {
    	public static void main(String[] args) {
    		
    		StringBuffer s1 = new StringBuffer();
    		StringBuffer s2 = new StringBuffer();
    		
    		//方式一
    		new Thread() {
    			@Override
    			public void run() {
    				synchronized (s1) {
    					s1.append("a");
    					s2.append("1");
    					
    					try {
    						Thread.sleep(100);
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    					
    					synchronized (s2) {
    						s1.append("b");
    						s2.append("2");
    						System.out.println(s1);
    						System.out.println(s2);
    					}
    				}
    			};
    		}.start();
    		
    		//方式二
    		new Thread(new Runnable(){
    			@Override
    			public void run() {
    				synchronized (s2) {
    					s1.append("c");
    					s2.append("3");
    					
    					try {
    						Thread.sleep(100);
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    					
    					synchronized (s1) {
    						s1.append("d");
    						s2.append("4");
    						System.out.println(s1);
    						System.out.println(s2);
    					}
    				}
    			};	
    		}) .start();
    		
    	}
    }
    

    线程通信

    线程通信常用方法
    • wait():定义在Object类,final,线程进入阻塞状态,释放锁(和sleep不同);
    • notify():定义在Object类,final,唤醒正在等待锁的线程,进入就绪状态(有优先级按优先级,没有随机唤醒一个);
    • notifyAll():定义在Object类,final,唤醒所有正在等待锁的线程;
    • 这三种方法只能出现在同步代码块或同步方法里,且不能用在lock里,否则会报错java.lang.IllegalMonitorStateException: current thread not owner
    • 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,默认情况下是this或者类.class(当前类的对象)
    sleep()和wait()的异同?

    同:一旦使用,均可使当前线程进入阻塞状态;
    异:

    • 声明位置不同:sleep()声明在Thread类中,wait()声明在Object类中;
    • 调用要求不同:sleep()可以使用在各种需要的地方,而wait()只能用在同步代码块或同步方法里;
    • sleep()使用不释放锁,而wait()使用后会释放锁。
    线程通信经典案例:生产者消费者问题
    /*
     * 线程通信经典问题:生产者与消费者
     * 生产者生产产品给店员,消费者从店员消费产品,店员一次只能固定最多持有一定数量的产品(比如:20件),
     * 当店员满额产品,生产者试图多生产产品时,店员会让生产者停一停;
     * 当店员产品不足,消费者试图继续消费时,店员会让消费者等一等;
     * */
    public class ThreadCommExe {
    	public static void main(String[] args) {
    		Clerk clerk = new Clerk();
    		Productor productor = new Productor(clerk);
    		Customer customer = new Customer(clerk);
    		Thread threadp = new Thread(productor);
    		Thread threadc = new Thread(customer);
    		threadp.setName("生产者线程1");
    		threadc.setName("消费者线程1");
    		threadp.start();
    		threadc.start();
    	}
    }
    
    //店员实际上是共享资源,生产者与消费者两个线程需要判断店员的存货情况
    class Clerk {
    	private int productCount = 0;
    
    	public synchronized void prodeceRespone() {
    		if(productCount<20) {
    			productCount++;
    			System.out.println(Thread.currentThread().getName()+"正在生产第"+productCount+"件产品");
    			notify();
    		}else {
    			try {
    				wait();
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    
    	public synchronized void customeRespone() {
    		if(productCount>0) {
    			System.out.println(Thread.currentThread().getName()+"正在消费第"+productCount+"件产品");
    			productCount--;
    			notify();
    		}else {
    			try {
    				wait();
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    }
    
    //生产者线程
    class Productor implements Runnable {
    	private Clerk clerk;
    
    	public Productor(Clerk clerk) {
    		this.clerk = clerk;
    	}
    
    	@Override
    	public void run() {
    		System.out.println("疯狂生产中......");
    		while(true) {
    			clerk.prodeceRespone();
    		}
    	}
    }
    
    //消费者线程
    class Customer implements Runnable {
    	private Clerk clerk;
    
    	public Customer(Clerk clerk) {
    		this.clerk = clerk;
    	}
    
    	@Override
    	public void run() {
    		System.out.println("疯狂消费中......");
    		while(true){
    			clerk.customeRespone();
    		}
    	}
    }
    

    注意事项

    • 单元测试需要特殊设置,不然不支持多线程验证。如果采用单元测试测试多线程,可能出现新线程执行不完整的情况,因为单元测试在main线程执行完之后自动System.exit()关闭java虚拟机,导致新线程无法继续执行
    我等的船还不来
  • 相关阅读:
    【LeetCode-回溯】组合总和
    MongoDB复制集成员类型
    Vant中的日期元素在iOS上显示NaN
    Vue风格
    Git设置代理和取消代理的方式
    吴晓波——疫情下的的“危”与“机”
    Vant库在PC端的使用
    买保险,不上当
    Vant的引入方式
    Duplicate keys detected: 'xxx'. This may cause an update error.
  • 原文地址:https://www.cnblogs.com/lxs1204/p/14350395.html
Copyright © 2011-2022 走看看