黑马程序员-java多线程

------- android培训java培训、期待与您交流! ----------

java之多线程
1.什么是多线程与并发编程

java提供了多线程的实现方式,什么是多线程,我们可以这么理解,在前面的笔记中,我们写的程序都是一条道走到黑,即单线程程序,cpu在为程序服务的时候,一直没有离开过.但是在生活中,这种行为比较少,比如说我们可以一边听音乐,一边玩游戏,一边喝茶,一边看电影,大脑在处理这些信息的时候丝毫不会出现混乱,所以程序的执行我们有时也希望他也能和我们人类一样,多个任务之间能够并发的执行,在单核cpu的硬件配置之下,cpu一会处理你,一会处理我,并发的执行,因为cpu处理数据的速度非常快,所以在我们用户看来,像是多个线程同时执行一样,这就是所谓的"微观串行,宏观并行"

2.名词解释
下面对几个名词进行解释
1).程序:程序就是指令和数据,存储在硬盘上,我们可以把他理解成静态不变的程序代码
2).进程:进程就是程序运行后的产物,是一个正在执行中的程序,如果有多个进程,进程与进程彼此之间相互独立,除非利用通道来通信,否则根本不知道彼此的存在.例如,我们通过windows任务管理器,能看到很多正在执行中的进程,我如果运行一个无限循环的java程序,任务管理器就会出现一个javaw.exe,这就是一个进程
3).多任务:多任务就是指多个进程任务同时进行,任务管理器中有很多进程在同时运行,操作系统将系统资源分配给各个进程,让进程之间交替运行
4).线程:将一个进程划分成最小的执行单位,这就是线程,让多个线程之间交替的运行,已获得更高的执行效率.与进程不同的是,同一个类的线程之间是共享同一块内存空间,所以有时又把线程说成负担轻的进程.


3.java中多线程的实现方式
java中实现多线程的方式有两种,一种是继承java.lang包中的 Thread 类,另外一种就是用户定义自己的类实现 Runnable 接口并覆写run方法,下面首先看一下第一种方式
1).在只需继承 Thread 类,并覆写run方法,调用start方法运行即可

实例1

public class ThreadDemo01  {

	public static void main(String[] args) {
		//创建两个线程类对象
		MyThread t1 = new MyThread("1线程");
		MyThread t2 = new MyThread("2线程");
		//启动两个线程对象
		t1.start();
		t2.start();
		for(int i = 1; i < 101; i++) {
			System.out.println(Thread.currentThread().getName() + i);
		}
	}

}
//自定义一个类,继承Thread类
class MyThread extends Thread {
	
	public MyThread(String name) {
		super(name);
	}
	//覆写run方法
	public void run() {
		for(int i = 1; i < 101; i++) {
			System.out.println(Thread.currentThread().getName() + "---" + i);
		}
	}
	
}

我们来分析这个程序
程序执行完t1.start()后ti线程启动了,但是不一定得到执行的机会,还有个主线程,t2.start()执行完后包括包括主线程在内,程序中有三个线程在运行,线程之间随意的切换运行
注意:切不可直接调用run方法,否者只是单纯的单线程方法调用。

2)下面来看一下实现 Runnable 接口的方式,

在java中也可以通过Runnable接口的方式实现多线程。Runable方法中指定义了一个抽象方法public void run() (其实Thread类也实现了这个接口)
实例2

//自定义Mythread类,实现Runnable
class MyThread implements Runnable {
	
	private String name;
	
	public MyThread(String name) {
		
		this.name = name;
	}
	//实现run方法
	public void run() {
		for(int i=1; i<101; i++) {
			System.out.println(this.name + "-----" + i);
		}
	}
}

public class RunnableDemo {
	public static void main(String[] args) {
		//定义两个线程类对象,并启动他们
		Thread t1 = new Thread(new MyThread("线程1"));
		Thread t2 = new Thread(new MyThread("线程2"));
		t1.start();
		t2.start();
		for(int i = 1; i < 101; i++) {
			System.out.println(Thread.currentThread().getName() + i);
		}
	}
}

我们现在来分析一下两种方式的优劣
两种方式虽然执行的效果大体相同,但是但是对于程序的可扩展性,明显第二种方式更佳,你想想看,如果你后期你继承Thread的类想要继承其他的类的话,将变得不可能,受到java单继承的
局限性,并且,还有一个问题,你如果想操纵共享数据的话,继承Thread的方式又把问题给暴露出来了,因为如果你将共享数据封装在Thread类中,这代表着你创建了几个线程对象,就有几份
共享数据,共享数据有且只能有一份,使用Runnable接口的方式的话,这种问题就不会出现.
2线程的状态
一个线程从创建到终止一共有5种状态,它们分别是:创建,就绪,运行,阻塞,终止.
 创建:是指一个线程线程被创建出来,还没有被调用,就是执行start()方法的时候的这段时期.
 就绪:是指一个线程创建并执行,但是还未真正的执行,处于一种可执行状态,进入线程池等待cpu调用它,是一种特殊的时期.
 运行:当就绪状态中的线程获得cpu资源,进入运行状态
 阻塞:由运行状态可转入阻塞状态,也可以转入消亡状态,一个正在运行的线程可能会因为wait()方法,yield()方法,sleep()方法,join()方法变为阻塞状态.阻塞状态解除后,线程进入线程
 池排队,重新变为就绪状态.
 消亡:消亡状态的产生有两个原因,一是run()方法执行完毕,正常的消亡,二是线程因为某些原因而停止运行.
线程的生命周期可以用下图来表示


4.线程类对象的一些常用的方法和线程优先级概念
线程的常用方法有以下几种
1.public static Thread currentThread()                                 //返回当前线程的引用
2.public final String getName()                                 //返回线程的名称
3.public final boolean isAlive()                                 //判断线程是否存活
4.public static boolean interrupted()                                 //判断当前线程是否中断
5.public void interrupt()                                 //中断线程。
6.public boolean isInterrupted()                                 //测试线程是否已经中断
7.public final void join() throws InterruptedException                  //暂停当前线程的执行,由调用该方法的线程一直执行,中间不能被别的线程打断,调用该方法的线程执行
  完毕后,当前线程才能执行
8.public final int getPriority()                 //返回线程的优先级
9.public final void setPriority(int newPriority)                 //设置线程的优先级
10.public static void sleep(long millis)throws InterruptedException     //当前线程睡眠指定的毫秒数
11.public static void yield() //暂停当前正在执行的线程对象,并执行其他线程.
实例3

class MyThread2 implements Runnable {
	
	//实现run方法
	public void run() {
		for(int i=1; i<101; i++) {
			System.out.println(Thread.currentThread().getName() + "-----" + i);
		}
	}
}

public class TestMethod {
	public static void main(String[] args) {
		//定义两个线程类对象,并启动他们
		Thread t1 = new Thread(new MyThread1(), "线程一");
		//Thread t2 = new Thread(new MyThread1(), "线程二");
		t1.start();
		//t2.start();
		for(int i = 1; i < 101; i++) {
			if(i == 10) {
				//当前线程的礼让,让t1执行
				Thread.yield();
			}
			if(i == 50) {
				try {
					//强制让当前线程暂停,t1线程执行完毕后,当前线程才能有资格运行
					t1.join();
					//当前线程睡眠五秒
					Thread.sleep(5000);
					//判断指定线程是否已经关闭
					System.out.println("t1  " + t1.isInterrupted());
					//判断当前线程是否关闭
					System.out.println("main  " + Thread.currentThread().interrupted());
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + i);
		}
	}
}
在这里说一下join()方法和yield()方法的区别,这两个方法容易混淆,前者是静态的,后者是非静态,除此之外,yield()可以理解为一种很礼貌的方式,我有执行权限,但是我不执行,让给你
类似于孔融让梨的典故,而join()是强制性的,调用join()的线程必须要等到那个线程执行完毕,当前线程才有资格运行,非常的霸道,显著的区别在与yield()方法让出去还抢的回来,join()
方法被别的线程抢过去之后,在他执行完之前怎么也抢不回来了.
接下来学习优先级
先看一段代码
实例4

package com.xiaogao.test;

public class TestThreadPriority {

	public static void main(String[] args) {
		MyTestThread mtt = new MyTestThread();
		Thread t1 = new Thread(mtt, "线程一"); 
		Thread t2 = new Thread(mtt, "线程二"); 
		Thread t3 = new Thread(mtt, "线程三"); 
		//分配最高的优先级
		t1.setPriority(Thread.MAX_PRIORITY);
		//分配最低的优先级
		t2.setPriority(Thread.MIN_PRIORITY);
		//分配默认的优先级
		t3.setPriority(Thread.NORM_PRIORITY);
		t1.start();
		t2.start();
		t3.start();
	}

}

class MyTestThread implements Runnable {

	public void run() {
			for(int i=1; i<101; i++) {
				System.out.println(Thread.currentThread().getName() + "-----" + i);
		}
	}
	
}

从打印的结果来看,明显线程一抢到的执行机会多,线程三次之,线程二最少,并不是说优先级高的一定最先抢到线程,只是抢到线程的概率大些而已


3多线程的安全与共享数据(synchronized关键字的使用)
当多个线程在操作同一资源时,为了保证操纵资源时的原子性,通常要在相关代码处加入同步功能,使线程操纵这段代码块时变成一个原子操作,什么是原子操作就是不可在分的意思,
放到程序中的意思就是一个线程访问一段代码的时候,中途不能被别的线程所打断要想一个代码块或一个方法变成一个原子操作,你得用 synchronized 修饰一个代码块,或者一个方法,
为了配合着 synchronized 使用的,java引入了一个概念叫互斥锁,什么是互斥锁,不要想得太过复杂,将互斥锁理解成权限的信物就行了,一个线程只有持有互斥锁才具有执行一段代码
的权利,没有互斥锁的话,只有等待正在执行中的线程执行完毕,执行完毕后,线程将会抛出互斥锁,别的线程才能重新获得锁,然后执行 synchronized 修饰的代码块,synchronized 有两
个使用格式
1 synchronized(锁对象) {
代码内容......
  }这里的锁对象可以是任何一个类的对象,也可以是一个字节码文件,这种方式可以显示的指定设置一把锁
2.public synchronized 返回值 变量名() {
方法体......
  }这种同步方法没有指名具体的锁对象,默认是this
还有,你如果在一个类的前面加上了 synchronized 这表示这个类中所有方法都是同步方法
实例3

/*
 *用于测试多线程操作统一资源时的安全性问题 
 */

public class TicketDemo  {

	public static void main(String[] args) {
		//创建四个线程类对象,并启动他们
		MyThread mt = new MyThread();
		Thread t1 = new Thread(mt);
		Thread t2 = new Thread(mt);
		Thread t3 = new Thread(mt);
		Thread t4 = new Thread(mt);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}



class MyThread implements Runnable {
	//定义100张票
	private int tick = 100;	
	//设置标志位,用于判断循环是否退出
	private boolean flag = true;	
	public void run() {
		
		while(flag) {
			synchronized(this) {
				if(tick>0) {
					try {
							Thread.sleep(50);	//睡眠50毫秒
					} catch (InterruptedException e) {
							e.printStackTrace();
					}
					System.out.println("第"+ tick-- +"张票售出");
					if(tick == 0) {
						flag = false;
					}
				}
			}
		}
	}
}

这段小程序如果不加同步的话,最后会出现负票卖出,这显然是我们不想看到的,加入同步代码后当一个线程睡眠后(同步代码块中的代码没有执行完毕),别的线程将无法进入。
同步功能的第二种表现形式,用synchronized关键字将一个方法变成同步方法。

实例4

/*
 * 用于测试同步方法
 */
public class SynchronizedDemo {

	public static void main(String[] args) {
		//创建四个线程类对象,并启动他们
		MyThread mt = new MyThread();
		Thread t1 = new Thread(mt);
		Thread t2 = new Thread(mt);
		Thread t3 = new Thread(mt);
		Thread t4 = new Thread(mt);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}

}

class MyThread implements Runnable {
	//票数初始化为100
	private int tick = 100;
	private boolean flag = true;
	
	public void run() {
		//当票数>0时,不断的卖票
		while(flag) {
			ticket();
		}
		
	}
	//定义同步方法
	public synchronized void ticket() {	
		//如果还有票可卖的话,睡眠80毫秒
		if(tick > 0) {
			try {
				Thread.sleep(80);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "第" + tick-- + "张票售出");
			if(tick == 0) {
				flag = false;
			}
		}
	}
}

总结:加入synchronized同步后,虽然增加了多线程的安全性,但是因为每次调用同步方法都要进行一次判断,所以效率会有所降低。
 
静态方法的锁,静态方法是属于类文件的,不属于对象,同步代码块中的锁不能使this,应该是类加载到内存所对应的字节码文件,以实例来说明
实例5

/*
 * 用于测试同步方法
 */
public class SynchronizedDemo {

	public static void main(String[] args) {
		//创建四个线程类对象,并启动他们
		MyThread4 mt = new MyThread4();
		Thread t1 = new Thread(mt);
		Thread t2 = new Thread(mt);
		Thread t3 = new Thread(mt);
		Thread t4 = new Thread(mt);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}

}

class MyThread4 implements Runnable {
	
	//票数初始化为100
	private static  int tick = 100;
	private static  boolean flag = true;
	
	public void run() {
		while(flag) {
			ticket();
		}
		
	}
	//定义同步静态方法,同步静态方法默认拿的是本类的.class,也就是字节码文件当锁
	public static synchronized void ticket() {		
		
			if(tick > 0) {
				try {
					Thread.sleep(80);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "第" + tick-- + "张票售出");
				if(tick == 0) {
					flag = false;
				}
			}
	}
}

总结:使用 synchronized 关键字的时候还要注意以下几点
 1.必须是多线程,单线程设置同步一点意义都没有
 2.互斥锁对象只能有一个,如果有多个的话,在执行同步代码块的时候,程序还是会并发的执行.
 3.在多线程中,如果出现同步代码块嵌套同步代码块,并且这个同步的锁不一样,就很容易出现死锁的情况,后面我们会讨论
4单例模式
什么是单例模式,有时候我们想让我们定义的类只能让客户端的程序员产生一个对象,不能创建多个对象,这时候就要考虑把构造方法私有化了,然后再定义一个静态成员变量,其实就是
一个常量,这个常量就是该类的唯一对象,然后再对外提供一个方法,该方法能够拿到这个唯一的对象.单例模式有两种设计方式
单例模式分为懒汉式和饿汉式
实例6

1饿汉式

public class SluggardDemo {

	private static final SluggardDemo sd = new SluggardDemo(); 
	
	//通过构造方法的私有化让他在其他类中无法创造对象
	private SluggardDemo() {	
		
	}
	//定义静态方法,返回本类的对象
	public static SluggardDemo getInstance() {
		return sd;
	}	
}

以上这种方式在开发上经常用到。
2懒汉式

public class SluggardDemo {

	private static final SluggardDemo sd = null; 
	
	//通过构造方法的私有化让他在其他类中无法创造对象
	private SluggardDemo() {	
		
	}
	
	public static SluggardDemo getInstance() {
		if(sd == null) {
			synchronized(SluggardDemo.class) {
				if(sd == null) {
					sd = new SluggardDemo();
				} 
			}
		}
		return sd;	
	}
}

饿汉式主要用在开发上,懒汉式因为受到多线程的影响,处理起来较为麻烦(不推荐使用),但面试可能会问到。
5多线程的死锁
什么叫死锁:举个例子,A有一只鞋,B也有一只鞋,A和B同时想外出,A想或得B的鞋,B想获得A的鞋,两个人僵持不下,谁都没能出去,在程序中的体现就是程序被挂起,无法继续运行。
实例7

/*
 *死锁程序 
 */
public class DeadLock {

	public static void main(String[] args) {
		//创建两个线程对象,并启动他们
		MyLockThread mt1 = new MyLockThread(true);
		MyLockThread mt2 = new MyLockThread(false);
		Thread t1 = new Thread(mt1);
		Thread t2 = new Thread(mt2);
		t1.start();
		t2.start();

	}

}

class MyLockThread implements Runnable {
	
	boolean flag;
	//定义构造方法
	public MyLockThread(boolean flag) {
		this.flag = flag;
	}
	//创建两个对象,把他们当做锁
	static Object obj1 = new Object();
	static Object obj2 = new Object();
	
	public void run() {
		
		if(flag) {
			synchronized(obj1) {
				System.out.println("B已经有了一只鞋");
				try {
					//睡眠50毫秒的理由是为了使t2这个线程能得到运行,让t2也持有一把锁,好出现死锁的情况
					Thread.sleep(50);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized(obj2) {
					System.out.println("B拿到了另外一只鞋");
				}
			}
		} else {			
			synchronized(obj2) {
				System.out.println("A已经有了一只鞋");
				try {
					Thread.sleep(50);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized(obj1) {
					System.out.println("A拿到了另外一只鞋");
				}
			}
		}
	} 
} 
6.线程之间的通信
线程与线程之间的通行远不止同步方法那么简单,有时情况会比较特殊,在同步代码块中,有时会需要使别的线程能执行同一把锁加密的另外一个方法,这时候,就应该把当前执行的线程
的互斥锁给抛出去,让那一个线程能够获得互斥锁对象,得以执行同步方法.这时候就要使用互斥锁提供的某些方法,因为互斥锁也是个对象,java不知道你会把什么类的对象用来当互斥锁
所以,抛出互斥锁的方法,在Object对象中,主要有以下三个
1.wait()
  对象调用wait()方法后,当前线程暂停执行,并将进入互斥锁的等待序列中,也就是阻塞状态,并且抛出互斥锁.
2.notify()
  唤醒正在等待对象互斥锁的众多线程中的一个线程,具体唤醒那个线程,这完全是随机的,这个线程将由阻塞状态变为就绪状态,线程执行后,从上次wait()的后面开始执行
3.notifyAll()
  唤醒正在等待对象互斥锁上的所有线程,优先级最高的将首先被唤醒并进入线程池,变为就绪状态,等待重新抢到互斥锁执行.
这里得注意一点,你基于某个对象调用以上三个方法中的任何一种,必须获得了该对象的互斥锁,也就是说,他们只能在同步代码块中被调用
生产者消费者问题
这是多线程中一个较为经典的问题,处理的不好的话,会出现数据错乱和程序停止的问题。
实例8
此程序中定义了一个资源类,一个生产者类,一个消费者,生产者负责生产笔记本,程序要保证每生产一台,消费者就消费一台
通过这个程序必须掌握 synchronized 配合 wait()和notify()方法的使用

/*
 * 用于测试生产者消费者问题
 */
public class TestProdurceConsumer {

	public static void main(String[] args) {
		//创建一个资源类对象
		Resource r = new Resource();
		//创建四个线程类对象,并启动他们
		Thread t1 = new Thread(new Produrce(r));
		Thread t2 = new Thread(new Consumer(r));
		Thread t3 = new Thread(new Produrce(r));
		Thread t4 = new Thread(new Consumer(r));
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}

}

class Resource {
	
	private String name;
	private boolean flag;
	
	//定义一个同步方法,负责生产笔记本
	public synchronized void set(String name) {		
		while(flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.name = name;
		System.out.println(Thread.currentThread() +"生产了" + "------" + name);
		flag = true;
		//唤醒其他正在线程池中等待的所有线程
		this.notifyAll();
	}
	
	//定义一个同步方法,负责消费笔记本
	public synchronized void get() {	
		//当线程多余两个时,为了防止出现重复生产及重复消费,把if换成while,但是可能会使程序中所有线程都在wait中
		while(!flag) {		
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread() + "消费了" + "-------------" + name);
		flag = false;
		//为了防止程序中所有线程都处于wait中,将notify()改成nitifyAll().
		this.notifyAll();	
	}
}
//定义生产者类
class Produrce implements Runnable{		
	
	private Resource r = null;
	
	public Produrce(Resource r) {
		this.r = r;
	}
	
	public void run() {
		for(int i = 1; i <= 100; i++) {
			r.set("笔记本" + i);
		}
	}
}
//定义消费者类
class Consumer implements Runnable{		
	private Resource r = null;
	
	public Consumer(Resource r) {
		this.r = r;
	}
	
	public void run() {
		for(int i = 1; i <= 100; i++) {
			r.get();
		}
	}
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值