Java多线程

 一、进程和线程的区别和联系

程序是指为完成特定的任务,用某种语言编写的一组指令的集合,即静态的代码

进程是指一种正在运行的程序或程序的一次执行过程,是一个动态的过程。如,运行中的QQ,运行中的播放器等。

线程是进程内部的一个执行单元,它是程序中一个单一的顺序控制流程。

进程和线程的区别:

注意:真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即一个cpu的情况下,在同一时间,cpu只能执行一个代码,由于时间片轮转很快,所以就有了 同时执行的错觉。

二、线程的创建

  • 方式1:继承Thread类,并重写run()方法。
  • 方式2:实现Runnable接口,并实现run()方法。

注意:方法run()称为线程体。

三、线程的启动(调用start方法!!!)

  • 新建的线程不会自动开始运行,必须通过start()方法启动。
  • 不能直接调用run()来启动线程,这样run()将作为一个普通方法立即执行,执行完毕前其他线程无法并发执行。
  • Java程序启动时,会立刻创建主线程,main()就是在这个主线程上运行。当不再产生新线程时,程序时单线程的。

方式1:继承Thread类,直接调用start()方法。

//创建线程
class Demo extends Thread{
    public void run(){
       //书写线程体
       ......
    }
}


//启动线程
Demo demo=new Demo();
demo.start();

Thread类中常用方法:

方式2:实现Runnable接口,通过创建Thread时将该对象作为参数传递,并启动

//创建线程
class Demo implements Runnable{
    public void run(){
       //书写线程体
       ......
    }
}


//启动线程
Demo demo=new Demo();
new Thread(demo).start();

解析:

  • 要想实现多线程必须实现Runnable接口,Thread类实现了Runnable接口。
  • Thread类中包含start()方法,而实现Runnable接口的实现类不包含start()方法,所以在启动线程时,需要通过Thread启动。
  • start()方法负责启动线程,其中start()方法由Java虚拟机调用此线程的run()方法,不能保证立即运行,只是加入调度器,底层由cpu按照时间片调度。

示例:

package com.csu.marden;
//方式1:继承Thread类
public class Demo3 extends Thread{
	@Override
	public void run() {
		for(int i=0;i<10;i++){
			System.out.println("一边学习。。。");
		}
	}
	public static void main(String[] args) {
		//创建子线程对象,并启动
		//该部分位于主线程内,系统启动主线程后,在主线程内启动子线程
		Demo3 demo3=new Demo3();
		demo3.start();
		
		//该部分属于主线程,由系统自动启动
		for(int i=0;i<10;i++){
			System.out.println("一边听歌。。。。。");
		}
	}
}





//方式2:实现Runnable接口
public class Demo3 implements Runnable{
	@Override
	public void run() {
		for(int i=0;i<10;i++){
			System.out.println("一边学习。。。");
		}
	}
	public static void main(String[] args) {
		//创建子线程对象,并启动
		//该部分位于主线程内,系统启动主线程后,在主线程内启动子线程
		Demo3 demo3=new Demo3();
		new Thread(demo3).start();
		
		//该部分属于主线程,由系统自动启动
		for(int i=0;i<10;i++){
			System.out.println("一边听歌。。。。。");
		}
	}
}

运行结果:

示例:模拟龟兔赛跑

package com.csu.marden;

//模拟龟兔赛跑
public class Demo4 implements Runnable{
	private static String winner;
	public Demo4(){
		
	}
	


    //run()方法才是线程体,多线程体现在run()方法上
    //该实例中,run()方法外的变量是共享的,而run()方法内的变量是单独的
	@Override
	public void run() {
		for(int step=1;step<=100;step++){
			//模拟兔子懒惰,线程睡眠100ms
			if(Thread.currentThread().getName().equals("兔子")){
				try {
					Thread.currentThread().sleep(100);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName()+"--->"+step);
			boolean flag=gameOver(step);
			if(flag){
				break;
			}
		}
	}
	
	public boolean gameOver(int step){
		if(winner!=null){
			return true;
		}else{
			if(step==100){
				winner=Thread.currentThread().getName();
				System.out.println("winner is---->"+winner );
				return true;
			}
		}
		return false;
	}
	
	
	
	public static void main(String[] args) {
		Demo4 demo=new Demo4();
		new Thread(demo,"乌龟").start();
		new Thread(demo,"兔子").start();
	}
	

}

运行结果:

四、线程的生命周期

要想实现多线程,必须在主线程中创建新的线程对象 。一个线程完整的生命周期通常经过一下五种状态:

  • 新建:当一个Thread类或其子类对象被声明创建时,新生的线程对象处于新建状态。
  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没有分配到CPU资源。
  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法是线程体,定义了线程的操作和功能。
  • 阻塞:在某种特殊情况下,被认为挂起或执行输入输出操作时,让出CPU资源并临时中止自己的执行,进入阻塞状态。
  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。

线程方法:

  • sleep():使线程停止运行一段时间,将处于阻塞状态。如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上回复执行。(抱着cpu资源睡觉)
  • join()阻塞指定线程等到另一个线程完成以后再继续执行。(a线程通过join方法插队到b线程中,阻塞b线程,等a线程执行完成后,才可以执行b线程)
  • yield():让当前正在执行线程暂停,不是阻塞线程,而是将线程转入就绪状态。调用了yield方法之后,如果没有其他等待执行的线程,这个时候当前线程会马上恢复执行。(释放cpu资源)
  • setDaemon():可以将指定的线程设置成后台线程,守护线程。创建用户线程的线程结束时,后台线程也随之消失。只能在线程启动之前把它设置为后台线程。
  • setPriority():设置线程的优先级,线程的优先级代表的是概率,范围从1到10,默认为5。
  • stop():停止线程,不推荐使用

五、线程同步

问题引出:

小王和老婆共同去银行取钱(同一个账号),银行卡当前余额为3000元,当小王和老婆共同进入账号的时候,会操作共享的余额。当每人取出2000元的时候,银行账号的余额为-1000元!!!

分析:

  • 多个线程执行的不确定性引起执行结果的不稳定性
  • 多个线程操作共享数据,会造成操作的不完整性,会破坏数据

示例:售票(线程不安全)

package com.csu.marden;

public class Window implements Runnable{
	private int ticket=100;

	@Override
	public void run() {
		while(true){
			if(ticket>0){
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+":售票,票号为:"+ticket);
				ticket--;
			}else{
				break;
			}
		}
		
	}
}
package com.csu.marden;

public class Demo1 {
	public static void main(String[] args) {
		Window window=new Window();
		Thread thread1=new Thread(window);
		Thread thread2=new Thread(window);
		Thread thread3=new Thread(window);
		
		thread1.setName("窗口1");
		thread2.setName("窗口2");
		thread3.setName("窗口3");
		
		thread1.start();
		thread2.start();
		thread3.start();
		
	}
}

问题:

卖票过程中,出现了错票,重票!

分析:

当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。

解决策略:

当一个线程a操作车票时,其他线程不能参与进来,直到线程a操作完成,其他线程才能参与进来。即使线程a出现了阻塞,也不能被改变。


1. 解决策略:synchronized关键字

  • 当两个或两个以上线程访问同一资源时,需要某种方式来确保资源在同一时刻只被一个线程使用。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
  • 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized),当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可。

(1)synchronized方法

public synchronized void method(){
    ......
}

synchronized方法控制对“成员变量/类变量”对象的访问:每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,知道从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。

缺陷:若将一个大的方法声明为synchronized,将会大大影响效率。

示例:售票(线程安全------使用synchronized关键字)

package com.csu.marden;



public class Window implements Runnable{
	private int ticket=100;

	public synchronized void show(){
		if(ticket>0){
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+":售票,票号为:"+ticket);
			ticket--;
		}
	}
	



	@Override
	public void run() {
		while(true){
			show();	
		}
	}
}
package com.csu.marden;

public class Demo1 {
	public static void main(String[] args) {
		Window window=new Window();
		Thread thread1=new Thread(window);
		Thread thread2=new Thread(window);
		Thread thread3=new Thread(window);
		
		thread1.setName("窗口1");
		thread2.setName("窗口2");
		thread3.setName("窗口3");
		
		thread1.start();
		thread2.start();
		thread3.start();
		
	}
}

(2)synchronized代码块

//语法
synchronized(obj){    //obj称为同步监视器
    ......
}
  • 同步块中的obj称为同步监视器,obj可以是任何对象,但是推荐使用共享资源作为同步监视器(要求多个线程必须使用同一把锁)
  • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,即该对象本身,或class,即类的模子。

同步监视器的执行过程:

  • 第一个线程访问,锁定同步监视器,执行其中代码
  • 第二个线程访问,发现同步监视器被锁定,无法访问
  • 第一个线程访问完毕,解锁同步监视器
  • 第二个线程访问,发现同步监视器未锁,锁定并访问

示例:售票(线程安全------使用synchronized关键字)

package com.csu.marden;



public class Window implements Runnable{
	private int ticket=100;


	@Override
	public void run() {
		while(true){
			
			
			synchronized (this) {
				if(ticket>0){
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()+":售票,票号为:"+ticket);
					ticket--;
				}else{
					break;
				}
			}
			
			
			
		}
	}
}
package com.csu.marden;

public class Demo1 {
	public static void main(String[] args) {
		Window window=new Window();
		Thread thread1=new Thread(window);
		Thread thread2=new Thread(window);
		Thread thread3=new Thread(window);
		
		thread1.setName("窗口1");
		thread2.setName("窗口2");
		thread3.setName("窗口3");
		
		thread1.start();
		thread2.start();
		thread3.start();
		
	}
}

2. 解决策略:Lock锁

  • 从JDK5.0开始,Java提供了更强大的线程同步机制---通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应当先获得Lock对象。
  • ReentrantLock类是Lock接口的一个实现类,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentranLock,可以显式加锁、释放锁。

示例:售票(线程安全-----使用ReentrantLock类)

package com.csu.marden;

import java.util.concurrent.locks.ReentrantLock;

public class Window implements Runnable{
	private int ticket=100;
	//1.实例化ReentrantLock
	private ReentrantLock lock=new ReentrantLock();

	@Override
	public void run() {
		while(true){
			
			try{
				//2.调用lock()方法-----锁住以后保证单线程执行
				lock.lock();
				if(ticket>0){
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()+":售票,票号为:"+ticket);
					ticket--;
				}else{
					break;
				}
			}finally{
				//3.调用unlock()方法----解锁
				lock.unlock();
			}
			
			
			
		}
		
	}
	

}
package com.csu.marden;

public class Demo1 {
	public static void main(String[] args) {
		Window window=new Window();
		Thread thread1=new Thread(window);
		Thread thread2=new Thread(window);
		Thread thread3=new Thread(window);
		
		thread1.setName("窗口1");
		thread2.setName("窗口2");
		thread3.setName("窗口3");
		
		thread1.start();
		thread2.start();
		thread3.start();
		
	}
}

3. synchronized关键字与Lock接口的异同

相同:

  • 两者都可以解决线程安全问题

不同:

  • synchronized关键字在执行相应的同步代码以后,自动地释放同步监视器;Lock接口(ReentrantLock实现类)需要手动启动同步,同时结束同步也需要手动解锁
  • Lock是显式锁(需要手动开启和关闭);synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁;synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好额扩展性(提供更多的子类)

优先使用顺序:

  • Lock
  • 同步代码块
  • 同步方法

4. 存在的问题:死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。魔偶一个同步块同时拥有多个synchronized同步锁时,就可能发生死锁的问题。比如在化妆的时候,小丫拿口红,大丫拿镜子,就会造成死锁。

  • 当两个线程相互等待对方释放锁时,就会发生死锁
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
  • 多线程编程时,若对多个资源上锁时,应该注意避免死锁的发生

示例:演示线程的死锁问题

package com.csu.marden;

public class Demo {
	public static void main(String[] args) {
		StringBuffer s1=new StringBuffer();
		StringBuffer s2=new StringBuffer();
		
		//线程1
		new Thread(){
			public void run() {
				//第一把锁
				synchronized (s1) {
					s1.append("a");
					s2.append("1");
					
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					
					//第二把锁
					synchronized (s2) {
						s1.append("b");
						s2.append("2");
						System.out.println(s1);
						System.out.println(s2);
					}
				}
			};
		}.start();
		
		
		//线程2
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				//第二把锁
				synchronized (s2) {
					s1.append("c");
					s2.append("3");
					
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					
					//第一把锁
					synchronized (s1) {
						s1.append("d");
						s2.append("4");
						System.out.println(s1);
						System.out.println(s2);
					}
				}
				
			}
		}).start();
		
		
		
		
		
		
	}

}

六、线程通信

背景:

  • 多个线程并发执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务时,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据
  • 如果没有线程通信来使多线程共同操作一份数据的话,虽然可以实现,但是很大程度上会造成多线程之间共享变量的争夺。

线程通信的方法:

  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
  • notify():一旦执行此方法,就会唤醒被wait阻塞的一个线程。如果有多个线程被阻塞,则唤醒优先级高的。
  • notifyAll():一旦执行此方法,就会唤醒所有被wait阻塞的线程。

注意:

  • wait,notify,notifyAll这三个方法必须使用在同步代码块同步方法中。
  • wait,notify,notifyAll这三个方法的调用者必须是同步代码块或同步方法中的同步监视器
  • wait,notify,notifyAll这三个方法都是定义在java.lang.Object类中。

sleep()和wait()的异同:

相同点:

  • 一旦执行方法,都可以使得当前的线程进入阻塞状态

不同点:

  • 两个方法的声明位置不同,Thread类中声明sleep(),Object类中声明wait()
  • 调用的位置要求不同,sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中
  • 关于是否释放同步监视器不同,如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放同步监视器,而wait()会释放同步监视器

示例:使用两个线程打印1-100,并且要求两个线程交替打印。

不使用线程通信:

package com.csu.marden;

public class Number implements Runnable{
	
	private int count=1;
	
	
	@Override
	public void run() {
		while(true){
			
				synchronized (this) {
					if(count<=100){
						try {
							Thread.sleep(100);
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
						System.out.println(Thread.currentThread().getName()+":"+count);
						count++;
						
						
					
					}else{
						break;
					}
				}
		}
		
	}
	

}
package com.csu.marden;

public class Demo2 {
	public static void main(String[] args) {
		Number number=new Number();
		
		Thread thread1=new Thread(number);
		Thread thread2=new Thread(number);
		
		thread1.setName("线程1");
		thread2.setName("线程2");
		
		thread1.start();
		thread2.start();
	}
}


使用线程通信:

package com.csu.marden;

public class Number implements Runnable{
	
	private int count=1;
	
	
	@Override
	public void run() {
		while(true){
			
			synchronized (this) {
				//唤醒另一个线程
				this.notify();
				if(count<=100){
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()+":"+count);
					count++;
					
					
					try {
						//阻塞当前线程
						this.wait();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}else{
					break;
				}
			}
		}
	}
}
package com.csu.marden;

public class Demo2 {
	public static void main(String[] args) {
		Number number=new Number();
		
		Thread thread1=new Thread(number);
		Thread thread2=new Thread(number);
		
		thread1.setName("线程1");
		thread2.setName("线程2");
		
		thread1.start();
		thread2.start();
	}
}


经典应用场景:生产者和消费者问题

问题描述:

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品。其中,店员一次只能持有固定数量的产品(比如:20)。如果生产者试图生产更多的产品,店员会叫生产者暂停一下,如果店中有空位放产品,再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者取走产品。

问题分析:

  • 该问题是多线程问题,包括生产者线程和消费者线程。
  • 该问题有线程安全问题,生产者线程和消费者线程会操作共享数据(产品数量)。
  • 该问题设计线程通信问题,需要生产者线程和消费者线程有规律地共同完成任务。

代码实现:

店员类:共享数据的操作

package com.csu.marden;

public class Clerk {
	//产品数量(生产者线程与消费者线程的共享数据)
	private int productCount=0;
	
	//生产产品
	public synchronized void produceProduct(){
		if(productCount<20){
			productCount++;
			System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个产品");
			notify();
		}else{
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	
	
	//消费产品
	public synchronized void consumeProduct(){
		if(productCount>0){
			System.out.println(Thread.currentThread().getName()+":开始消费第"+productCount+"个产品");
			productCount--;
			notify();
		}else{
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

}

生产者类:

package com.csu.marden;

public class Productor extends Thread{
	private Clerk clerk;
	
	public Productor(Clerk clerk){
		this.clerk=clerk;
	}
	
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"开始生产产品...");
		while(true){
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			clerk.produceProduct();
		}
	}

}

消费者类:

package com.csu.marden;

public class Customer extends Thread{
	private Clerk clerk;
	
	public Customer(Clerk clerk){
		this.clerk=clerk;
	}
	
	
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"开始消费产品...");
		while(true){
			try {
				Thread.sleep(20);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			clerk.consumeProduct();
		}
	}

}

测试类:

package com.csu.marden;

public class Test {
	public static void main(String[] args) {
		Clerk clerk=new Clerk();
		
		Productor p1=new Productor(clerk);
		p1.setName("生产者1");
		
		Customer c1=new Customer(clerk);
		c1.setName("消费者1");
		
		p1.start();
		c1.start();
	}

}

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值