这几天学的Java多线程,多线程涉及的挺多,但我只是学的基础部分,写一篇博客,也算给自己整理一下吧。以后有空也可以多回顾一下。好吧,go!
一.进程与线程
学计算机的同学都学过操作系统这门课,可惜我不是计算机专业的。但是进程这个概念不一定要计算机系的同学才能了解,平时用操作系统的任务管理器都可以了解。
给进程下个定义,那就是指操作系统进行资源分配和调度的基本单位,如打开qq,qq在后台就是一个进程,或者打开浏览器,浏览器可能会有多个进程。
所谓线程:是程序使用cpu的基本单位。多个线程就是多条执行路径。
Java程序是运行在Java虚拟机上的,这个大家都知道。java程序启动也会启动JVM,也相当于启动了一个进程。而且,一个Java程序会至少会启动一个进程。
jvm是多线程,如何理解?因为还有个GC
线程有两个调度模型,一个是CPU分时调度,还有一个是抢占式。java的多线程就是属于抢占式,如何理解呢,就是调度哪个,什么时候调度是由CPU决定的。多线程中,假如A线程执行到一半,可能会中途被CPU挂起,然后CPU去执行线程B,同样在执行线程B中,也可能挂起,然后去执行线程C。
二、线程的实现
两种方式
1、继承Thread类,重写run方法,新建实例,调用start方法
2、类A实现Runnable接口,重写run方法,A类实例化,新建一个Thread实例,同时将A类对象传入,Thread子类调用start方法。
看代码吧
第一种方式
public class ThreadDemo { public static void main(String[] args) { // 3、实例化一个对象 MyThread Luna=new MyThread(); MyThread Mars=new MyThread(); Luna.setName("Luna-----"); Mars.setName("Mars-----"); // 4、调用对象的run()方法 Luna.start(); Mars.start(); } } //1、继承Thread类 class MyThread extends Thread{ // 2、重写父类的run()方法 @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(getName()+"-----"+i); } } }
第二种方式,实现runnable接口
public class RunThread { public static void main(String[] args) { MyRunThread runThread=new MyRunThread(); Thread thread1=new Thread(runThread, "Luna"); Thread thread2=new Thread(runThread, "Mars"); thread1.start(); thread2.start(); } }//1.实现runnable接口 class MyRunThread implements Runnable{ //2.重写run方法 @Override public void run() { for (int i = 0; i < 30; i++) { System.out.println(Thread.currentThread().getName()+"-----"+i); } } }
这里要说明的一点是,其实Thread类也是实现了Runnable接口的。
三、线程控制及优先级
Java提供了挺多的方法,简单说几个常用的
线程睡眠:sleep(long millis),睡眠多少毫秒.会抛出异常InterruptedException.这个异常会在interrupt( )方法后抛出
线程加入:public final void join(),指的是将线程合并到另一个线程,如合并到main线程,这个也会造成线程阻塞。
线程礼让:public static void yield()指的是让另一个线程先执行,至于是哪个线程,就看CPU决定
线程优先级,由1-10,默认是5,可以通过setPriority()方法设置优先级.但是这个优先级只是在java程序中有优先级,CPU并不一定会是优先执行.
四、线程安全
上面是从百度百科里面参考的,我觉得表达的比较清楚。总结一下发生线程不安全的原因
1.前提肯定是要有多线程
2.有数据共享
3.多线程对共享数据的操作不是原子操作。
理解一个概念,什么是原子操作?原子在物理学中,看作是不可再分的,所谓原子操作就是要么执行完毕,要么不执行。为何呢,前面说了,多线程时候,CPU可能执行到中途,会把当前线程挂起,然后去执行其他线程。所以在执行共享数据代码时候执行到某个时刻,停了一下,这时候其他线程又来执行。所以出现了线程不安全的问题。
理解了出现线程不安全的原因,那么就容易解决了。既然不是原子操作,那就将共享数据代码块编程原子操作,用synchronized关键字,加大括号可以将代码锁定
在使用synchronized关键字时候,要传入一个对象,这个对象作为一个锁,如果要锁定,那就注意这个锁一定要是唯一的,不能有多个锁。
好吧,贴一段代码。
import java.util.Scanner; /* 请实现一个类,继承自Thread,来实现模拟迅雷多线程下载程序。 要求:迅雷每个线程可以下载1M的资源。对一个文件大小为x M的资源,进行下载, 动态调整下载线程的个数(命令行输入下载的资源 和 希望启动的下载线程个数)。当下载完成时,提示用户下载任务完成。(可以再命令行输出提示) PS:完成过程中可以分别使用同步代码块 和同步方法来解决程序中可能出现的线程安全问题。*/ public class ThunderDemo { static Object object=new Object(); public static void main(String[] args) { Scanner sc=new Scanner(System.in); System.out.println("请输入下载资源:_ _ _"); int recSize=sc.nextInt(); DownLoad.size=recSize; System.out.println("请输入线程个数:_ _ _"); int threadCount=sc.nextInt(); sc.close(); for (int i = 0; i < threadCount; i++) { DownLoad downLoad4 = new DownLoad(); downLoad4.start(); } } } class DownLoad extends Thread { static int size; static int downed = 0; boolean flag = true; @Override public void run() { while (size - downed >= 0) { //!!!!!!!严重注意,这里synchronized如果传了this,则调用new thread的时候不是同一把锁,而是多把!!!!!! synchronized (ThunderDemo.object) { if (size - downed >= 0) { float percent = (float) ((downed * 1.0 / size) * 100.0); System.out.println( getName() + "已下载---" + downed + "M,---剩余---" + (size - downed) + "----" + percent + "%"); downed++; if (size - downed < 0) { System.out.println("DownLoad Complete!!------100%"); } try { sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }
使用线程同步固然可以消除线程不安全问题,但同时也有一些缺陷,就是消耗资源(当线程比较多的时候,每次都要去判断同步锁),影响效率;还有一点是如果出现嵌套锁,就会容易出现死锁。
还有就是synchronized关键字可以放在方法声明中,这是表示的就是锁定当前方法了.
五、死锁问题
好吧,又是操作系统这门课中有提到的死锁,但我们这里说的线程死锁。并非操作系统中的死锁。想当初看中山大学考研复试经验中提的比较多的就是死锁问题,看来还挺重要的。
所谓死锁(线程的),就是指两个线程锁,互相抢占了对方的资源,导致线程无法继续运行。如A拿到了资源a,再拿到资源b,就可以执行完,而B拿到了资源b,再拿到资源a就可以执行完。但是双方的原则都是要先执行完自己的线程才能释放资源,这就造成了线程死锁
照例贴一段代码吧
public class MyThread extends Thread { @Override public void run() { if (getName().equals("中国人")) { //中国人进来应该执行这段代码,拿到锁A synchronized (MyLock.LockA) { System.out.println(getName() + "MyThread.run() 我得到了A锁,要继续执行的话需要B锁" ); synchronized (MyLock.LockB) { System.out.println(getName() + "MyThread.run() 我得到了B锁" ); //xxdxd System.out.println(getName()+"MyThread.run() 两把锁都得到,开始吃饭"); } } }else { //英国人进来,需要执行这段代码 synchronized (MyLock.LockB) { System.out.println(getName() + "MyThread.run() 我得到了B锁,要继续执行的话需要A锁" ); synchronized (MyLock.LockA) { System.out.println(getName() + "MyThread.run() 我得到了A锁" ); //xxdxd System.out.println(getName()+"MyThread.run() 两把锁都得到,开始吃饭"); } } } } //穿入一个字符串,会使用该字符串去设置名字 public MyThread(String name,Object obj){ super(name); //setName(name) //this.obj=obj; } } public class MyLock { static Object LockA = new Object(); static Object LockB = new Object(); } public class Main { public static void main(String[] args) { Object object = new Object(); MyThread t1 = new MyThread("中国人",object); MyThread t2 = new MyThread("英国人",object); t1.start(); t2.start(); } }
六、线程同步机制(生产者消费者问题)
首先,当线程在继续执行前需要等待一个条件方可继续执行时,仅有 synchronized 关键字是不够的。因为虽然synchronized关键字可以阻止并发更新同一个共享资源,实现了同步,但是它不能用来实现线程间的消息传递,也就是所谓的通信。
而在处理此类问题的时候又必须遵循一种原则,即:对于生产者,在生产者没有生产之前,要通知消费者等待;在生产者生产之后,马上又通知消费者消费;对于消费者,在消费者消费之后,要通知生产者已经消费结束,需要继续生产新的产品以供消费。
七、线程的完整生命周期
线程生命周期(主要)
新建:就是创建了线程对象
就绪:当线程对象调用了start方法
运行:获得CPU执行权,开始执行,阻塞:没有执行资格,不执行
死亡:执行完毕,等待垃圾回收
嗯,差不多就这么多,想起来重要的知识点会继续补充。
写博客真的挺耗时间的,而且要写得好的话需要更多时间更多能力。