一,多线程概念
要掌握多线程,首先你需要了解进程和线程的概念,以及两者之间的关系。
1,进程
进程是程序在处理机中的一次运行。一个进程既包括其所要执行的指令,也包括了执行指令所需的系统资源,不同进程所占用的系统资源相对独立。所以进程是重量级的任务,它们之间的通信和转换都需要操作系统付出较大的开销。
简单来说,进程就是一个正在执行的程序。
每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
2,线程
线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程自己基本上不拥有系统资源,但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。所以线程是轻量级的任务,它们之间的通信和转换只需要较小的系统开销。
Java支持多线程编程,因此用Java编写的应用程序可以同时执行多个任务。
3,线程和进程之间的关系
线程就是进程中的一个独立的控制单元。线程在控制着进程的执行。只要进程中有一个线程在执行,进程就不会结束。
一个进程中至少有一个线程。
4,结论
了解了进程和线程以及两者之间的关系,我们就可以得出这样的结论:
单线程:一个进程中只有一个执行路径,即一个线程,那么这个程序称为单线程。
多线程:一个进程中有多个执行路径,即多个线程,那么这个程序就被称为多线程。
5,多线程的好处(意义)
为什么出现多线程?
举个例子:从磁盘读取一个文件需要5秒,处理一个文件需要2秒。
单线程执行顺序:读取文件A、处理文件A、读取文件B、处理文件B。
单线程实现这样的操作共需要14秒,可以说效率非常低,因为从磁盘中读取文件的时候,大部分的CPU时间用于等待磁盘去读取数据。在这段时间里,CPU非常的空闲,可以让它可以做一些别的事情。
通过改变操作的顺序,就能够更好的使用CPU资源。如:
读取文件A、读取文件B(A还未读取完)、处理文件A(B未读取完)、处理文件B。
按照这样的顺序执行只需要12秒。
这样我们就得出了结论:多线程的出现能让程序产生"同时运行"效果。可以提高程序执行效率。
注意:随机性原理,CPU在某一个时刻只能执行一个程序,多个程序执行只是CPU快速的切换完成的。二,java实现多线程
讲完了多线程的概念,你应该对多线程有了初步的了解,下面说一下java是怎么实现多线程的。
java中创建线程有两种方式:继承Thread类和实现Runnable接口。
1,继承Thread类
步骤:
a,定义类继承Thread类。
b,覆盖Thread类中的run方法,将需要被多线程执行的代码定义到该run方法当中。
c,建立Thread类的子类创建线程对象。
d,调用start方法,开启线程并调用该线程的run方法。
下面有个示例来让你直观的了解怎么用继承Thread类的方式来创建线程。<span style="white-space:pre"> </span>/*
* 示例:创建三个线程,每过2秒打印一下线程的名称,打印三次
*/
public class Thread1 extends Thread{
private final int MAX = 3;//最大打印次数
private int COUNT = 1;//计数
private final int TIME = 2;//间隔时间
//接收线程名称
public Thread1(String name) {
super(name);
}
//覆盖run方法,在里面写我们要执行的代码
public void run() {
while(COUNT<= MAX){
System.out.println(this.getName());
COUNT++;
//每次打印后,在一段时间后再打印
try {
Thread.sleep(TIME*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread1 t1 = new Thread1("线程1");//创建线程
Thread1 t2 = new Thread1("线程2");
Thread1 t3 = new Thread1("线程3");
t1.start(); //开启线程
t2.start();
t3.start();
//也可以使用下面这种方式书写
//new Thread1("线程4").start();
}
}
下图是执行结果:观察可知,多线程的执行是随机交替执行的,每次运行的结果都可能不同。
看到这里,有些朋友可能会有疑问:为什么要继承Thread?为什么要覆盖run方法?
其实直接建立Thread类对象即可。并开启线程执行就可以了。但是虽然线程执行了,可是执行的代码是该线程默认的代码,该代码就存放在run方法中。我们定义线程的目的是为了执行自定义的代码,实现自己想要的功能,而线程运行代码都存储在run方法中,所以只有覆盖了run方法,才可以运行自定义的内容,想要覆盖,必须先要继承。
主线程运行的代码都在main函数中,自定义线程运行的代码都在run方法中。
2,实现Runnable接口
使用继承方式有一个弊端,那就是如果该类本来就继承了其他父类,那么就无法通过Thread类来创建线程了,这样就有了第二种创建线程的方式:实现Runnable接口。
步骤:
a, 定义类实现Runnable的接口。
b, 覆盖Runnable接口中的run方法。目的也是为了将线程要运行的代码存放在该run方法中。
c, 通过Thread类创建线程对象。
d, 将Runnable接口的子类对象作为参数传递给Thread类的构造方法。
e, 调用Thread类中start方法启动线程。start方法会自动调用Runnable接口子类的run方法。
实现Runnable接口好处:java是单继承多实现,使用实现Runnable接口的方式避免了单继承的局限性。在定义线程时,建议使用实现Runnable接口方式。
/*
* 示例:创建三个线程,每过2秒打印一下线程的名称,共打印三次
* */
class ThreadTestWin implements Runnable{
private final int MAX = 3;//最大打印次数
private int COUNT = 1;//计数
private final int TIME = 2;//间隔时间
private String NAME;
public ThreadTestWin(String name) {
this.NAME = name;
}
public String getName() {
return this.NAME;
}
//覆盖run方法,在里面写我们要执行的代码
public void run() {
while(COUNT<= MAX){
System.out.println(this.getName());
COUNT++;
//每次打印后,在间隔时间后再打印
try {
Thread.sleep(TIME*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Thread2{
public static void main(String[] args) {
Thread t1 = new Thread(new ThreadTestWin("线程1"));//创建线程
Thread t2 = new Thread(new ThreadTestWin("线程2"));
Thread t3 = new Thread(new ThreadTestWin("线程3"));
t1.start(); //开启线程
t2.start();
t3.start();
//也可以使用下面这种方式书写
//new Thread(new Thread2("线程4")).start();
}
}
执行结果如图:
为什么将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数?
因为要被多线程执行的代码都存放了Runnable接口的子类中,所以必须要明确线程对象要执行的run方法所属的对象。
3,这两种方式的区别:
a,继承Thread:线程代码存放在Thread子类run方法中; 实现Runnable:线程代码存放在接口子类run方法中。
b,实现Runnable接口是可以将资源共享。
c,实现Runnable接口避免了单继承的局限性 。
所以建议使用实现Runnable接口的方式来创建线程。
三,线程状态
创建状态:通过建立Thread类的对象或者Thread类的子类对象,来完成线程创建。
运行状态:线程具备执行资格,具备CPU执行权,表示该线程正在被CPU执行。
临时阻塞状态:只具备执行资格,不具备执行权。
冻结状态:释放了线程执行权和线程的执行资格,
线程执行到sleep方法或wait方法时,线程就会进入这种状态。
消亡状态:当线程调用了stop方法(过时),或者线程执行的代码已经结束了,
这时该线程结束,该执行路径在进程中消失。
四,多线程的缺点
a,等候使用共享资源时造成程序的运行速度变慢。这些共享资源主要是独占性的资源,如打印机等。
b,对线程进行管理要求额外的CPU开销。线程的使用会给系统带来上下文切换的额外负担,当这种负担超过一定程度时,多线程的特点主要表现在其缺点上,比如用独立的线程来更新数组内每个元素。
C,线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状。
d,对公有变量的同时读或写。当多个线程需要对公有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,从而使前一个线程的参数被修改;另外,当公用变量的读写操作是非原子性时,在不同的机器上,中断时间的不确定性,会导致数据在一个线程内的操作产生错误,从而产生莫名其妙的错误,而这种错误是程序员无法预知的。
五,线程同步
为了解决多线程引发的安全问题,java引入了线程同步synchronized。
同步原理:通过一个对象锁,将多条操作共享数据的代码进行了封装并加锁,持有这个锁的线程才能进入同步去执行,执行期间,其他线程无法获得执行权。
Java中线程同步有两种方式:同步代码块和同步函数。
1,同步代码块
使用方法:
synchronized(对象){
需要被同步的代码
}
同步代码块之所以能够解决安全问题,原因就在那个对象上。这个对象就像一把钥匙,拥有钥匙的线程可以打开锁执行同步代码,没有钥匙的线程即使获取了cpu的执行权,也不能执行同步代码。
同步代码块示例:
/*
* 多线程打印1-100,并打印当前线程名称
* */
class ThreadTestWin2 implements Runnable{
private int count = 1;
private int MAX = 100;
Object Obj = new Object();
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
synchronized (Obj) {
if(count<=MAX){
System.out.println(Thread.currentThread().getName()+"____"+count);
count++;
try {
Thread.sleep(30);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
}
}
}
class Thread3{
public static void main(String[] args) {
ThreadTestWin2 t = new ThreadTestWin2();//创建线程
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.start(); //开启线程
t2.start();
t3.start();
}
}
2,同步函数
将同步关键字(synchronized)修饰在函数上即可。
同步函数示例:
/*
* 多线程打印1-100,并打印当前线程名称
* */
class ThreadTestWin3 implements Runnable{
private int count = 1;
private int MAX = 100;
Object Obj = new Object();
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
show();
}
}
public synchronized void show(){
if(count<=MAX){
System.out.println(Thread.currentThread().getName()+"..。"+count);
count++;
try {
Thread.sleep(30);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
}
class Thread4{
public static void main(String[] args) {
ThreadTestWin3 t = new ThreadTestWin3();//创建线程
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.start(); //开启线程
t2.start();
t3.start();
}
}
3,同步的前提
a,必须是两个或者两个以上的线程。
b,必须保证多个线程使用的是同一个锁。
注意:如果你发现多线程存在安全问题,而且加上同步后,安全问题没有解决,那么要查看一下这两个前提是否符合。
4,同步具体体现区别:
同步函数使用的锁是this。
同步代码块使用的锁可以是任意对象。
特殊:静态同步函数使用的锁是 该函数所属类对象字节码文件对象。
六,线程间通信
线程间通信其实就是多个线程操作同一个资源。
面的示例采用同步操作同一资源:/*
* 例子:两个卖方共同经营一家手机店(资源),生意很兴隆,每到货一部手机,就能立即卖出去
* */
public class Thread5 {
public static void main(String[] args)
{
Res r = new Res();//表示操作的是同一个资源
Pro p = new Pro(r);
Cus c = new Cus(r);
Thread t1 = new Thread(p);//t1,t2是卖方
Thread t2 = new Thread(p);
Thread t3 = new Thread(c);//t3,t4是买方
Thread t4 = new Thread(c);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Res
{
private String name;
private int count = 0;
private boolean b = false;
public synchronized void set(String name)
{
while(b)
try{this.wait();}catch(Exception e){} //有手机等待手机卖出
this.name = name+"_"+count;
count++;
System.out.println(Thread.currentThread().getName()+".....买方...."+this.name);
b = true; //切换为有手机
this.notifyAll();//唤醒
}
public synchronized void out()
{
while(!b)
try{this.wait();}catch(Exception e){} //无手机,等待到货
System.out.println(Thread.currentThread().getName()+".....卖方...."+this.name);
b = false; //切换为无手机状态
this.notifyAll();
}
}
//实现Runnable接口实现多线程
class Pro implements Runnable
{
private Res r;
Pro(Res r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.set("phone");
}
}
}
class Cus implements Runnable
{
private Res r;
Cus(Res r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.out();
}
}
}
输出结果(部分截图):
关于上面的代码,解释几点:
1,wait(),sleep()有什么区别?
sleep()是线程类(Thread)的方法,让出cpu,让cpu去执行其他线程,在指定的时间过后,cpu回到这个线程继续执行。
wait()是Object类的方法,调用wait方法会导致线程放弃对象锁,进入等待锁定池,只有发出notify方法或notifyAll后线程才进入对象锁定池准备获得对象锁进入运行状态。
总结来说:
wait():释放cpu执行权,释放锁。
sleep():释放cpu执行权,不释放锁。
2,notify()和notifyAll()的区别?
notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。
notify(): 唤醒一个正在等待该对象的线程。
notifyAll(): 唤醒所有正在等待该对象的线程。
两者的最大区别在于:
notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
notify他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁,此时如果该对象没有再次使用notify语句,即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。
七,单线程、多线程什么时候使用?
多线程效率确实比单线程效率高,但使用也是要分情况的,一些情况下并不适合用多线程。
1.耗时的操作使用线程,提高应用程序响应。如读取数据库文件、导入数据。
2.并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求。
3.多CPU系统中,使用线程提高CPU利用率
4.改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
其他情况都使用单线程。
------- android培训、 java培训、期待与您交流! ---------