理解一下Java基础中的线程,为自己做一个总结
(依靠Java基础入门这个书学习,所以大致的方向也是按书上来的)
重点如下
多线程的概念
线程创建的两种方式
线程的生命周期及状态转换
线程的调度
线程的安全和同步
多线程通信
多线程的概念
在Windows系统
上,同时运行着许多的进程,而进程下就是线程
要理解线程,先知道进程
进程是指一个内存中运行的应用程序,每个进程都有自己独立的内存空间,一个进程可以启动多个线程
动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
并发性:任何进程都可以同其他进程一起并发执行
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
异步性:每个进程都以相互独、不可预知的速度向前推进
结构特征:进程由程序、数据和进程控制块三部分组成。
(个人的理解)线程支撑其进程,系统分配给进程的内存空间给予线程使用
线程是进程中的一个执行流程,一个进程中可以运行多个线程
线程创建的两种方式
一个程序要运行起来必须要有一个线程,Java也是如此
public class OneDay01 {
public static void main(String[] args) {
System.out.println("哈哈哈");
}
}
这么简单的Java程序也是有线程的,它的称之为主线程也是main线程
如果想在此基础上创建再创建一个线程的话,就需要利用JDK中提供的一个线程类
Thread
,通过继承Thread,并重启Thread类中的run()方法
public class OneDay01 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for(;;){
System.out.println("main线程在运行");
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for(;;){
System.out.println("MyThread线程在运行");
}
}
}
注意:一定要记得在继承了Thread类中重写run()方法,里面的逻辑就是要实现进程的功
在创建这个类的实例化后要记得.start() 运行其方法
这是第一种,下面是第二种 实现Runnable接口
public class OneDay01 {
public static void main(String[] args) {
MyRunable myRunable = new MyRunable();
Thread thread = new Thread(myRunable);
thread.start();
for(;;){
System.out.println("main线程在运行");
}
}
}
class MyRunable implements Runnable{
public void run() {
for(;;){
System.out.println("MyRunable线程在运行");
}
}
}
同上一样,只是让其看得清楚点
public class OneDay01 {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
for(;;){
System.out.println("Thread线程在运行");
}
}
}).start();
for(;;){
System.out.println("main线程在运行");
}
}
}
一种是继承Thread类,一种是实现
Runnable接口
通过Thread类实现了多线程,但是这种方式有一定的局限性,因为Java中只支持单继承,一个类一旦继承某个父类就无法再继承Thread类,
比如学生类Student继承了一个Person类,就无法通过继承Thread类创建线程。为了克服这种弊端,Thread类提供了另一个
构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run()方法。
当通过Thread(Runnbale target)构造方法创建线程对象时,只需要为该方法传递一个实现Runnable接口的实例对象,
这样创建的线程将调用实现了Runnble接口中的run()方法作为运行代码,而不要调用Thread类中run()方法。
两者的区别:
第一种继承Thread类,然后再创建一个实例,如果想创建多个实例这样的也就是创建了多个同样的类,这几个内功能一样,处理的数据一样,但是不会相互影响
可以想象成这是跑百米:每一个线程每一个实例也就是这条跑道上的运动员,但是它们跑也只是在自家跑道上跑。不会跑到别人那去
而第二种线程就是 是实现一个接口Thread thread = new Thread(myRunable);再创建一个对象也就是创建
new Thread(myRunable);而
myRunable是不变的,创建无数个对象
之后也是只有一个
myRunable。可以想象成这几个线程是在跑3000米,公用一个塑胶跑道但是争用内道
在程序开发中只要是多线程肯定永远以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下好处:
避免点继承的局限,一个类可以继承多个接口。
适合于资源的共享
两种的区别推荐看看这个:http://developer.51cto.com/art/201203/321042.htm
线程的生命周期及状态转换
这个是理论功夫,我就添张图
Java线程具有五中基本状态
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程的调度
程序中的多个程序是并发执行的,某个程序若想被执行必须得到CPU的使用权。Java须虚拟机会按照特定的机制为程序中的每一个程序分配CPU的使用权,这样的机制叫做调度。
在计算机中,线程调度有两种模型,分别是分时调度和抢占式调度模型-
分时调度模型是指让所有的线程轮流获取CPU的使用权,并且平均分配每个线程占用CPU的时间片
抢占式模型是指让可运行池中优先级高的线程优先占用CPU。而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,在随机选择其他线程获取CPU的使用权
线程的优先级
在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU的机率执行的机会就会越大,而优先级越低的线程获取CPU的执行的机会就会越小
线程的优先级用1~10之间的整体来表示,数字越大优先级越高
除开可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量来表示
Thread 类的静态常量 static int MAX_PRIORITY 10
Thread 类的静态常量 static int MIN_PRIORITY 10
Thread 类的静态常量 static int NORM_PRIORITY 10
程序在运行期间,处于就绪状态的每一个线程都有自己的优先级,列如main线程具有普通优先级。然而线程优先级不是固定不变得,可以通过Thread类的setPriority(int newPriority)方法对其进行设置,该方法中的参数newPriority接受
1——10之间的整数或者Thread类的三个静态常量
需要注意的是,虽然Java中提供了10个线程优先级。但是这些优先级需要操作系统的支持,不同的操作系统对优先级的支持是不一样的,不能很好的和Java中线程优先级一一对应,因此,在设计多线程应用程序时,其功能的实现不一定能依赖于线程的优先级,而只能把线程优先级作为一种提高程序效率的手段
线程的休眠
在程序运行时线程想休眠一段时间,可以使用静态方法sleep(),这个方法可以让当前正在执行的线程暂停一段时间,进入休眠状态。
这样也就可以让其他线程运行,因为休眠的时候拿CPU的使用权交了出去
当休眠时间结束之后,线程会进入就绪状态,不会直接运行
线程的让步和插队
暂时我觉得没有什么好讲得
知道让步是.yield();
插队是.join();
线程的安全和同步
这一段 抄书了
多线程并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题
例如:
package com.yancan.run4;
public class Example01 {
public static void main(String[] args) {
Example example = new Example();
new Thread(example,"售票窗口yi").start();
new Thread(example,"售票窗口er").start();
new Thread(example,"售票窗口san").start();
new Thread(example,"售票窗口si").start();
}
}
class Example implements Runnable {
private int number = 10;
public void run() {
while(number>0){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"----"+number--);
}
}
}
售票窗口yi----8售票窗口san----7售票窗口si----9售票窗口er----10售票窗口san----6售票窗口er----3售票窗口yi----5售票窗口si----4售票窗口si----2售票窗口yi----1售票窗口er----0售票窗口san-----1售票窗口si-----2
出现了负一的情况是不对的。
在售票程序中while循环添加了sleep()方法,这样就模型了售票过程的延迟。由于线程由延迟,当票号减为1时,假设1此时出售1号票,对票号进行判断后,进入while循环,在售票之前通过sleep()方法让线程二会进行售票,由于此时票号任然为1,因此线程二也会进入循环,同理,四个线程都会进入while循环,休眠结束后,四个线程都会进行售票,这样就相当于将票号减了四次。
【5.5.2】同步代码块
了解到线程安全问题其实就是由于多个线程同时处理共享资源所导致的,想要解决线程中的安全问题,必须得保证用于处理共享资源的代码在任何时刻只能有一个线程访问
为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码块,其语法格式如下
synchronized(lock){
操作共享资源代码块
}
上面的代码中,lock是一个锁对象,它是同步代码块的关键,当线程执行同步代码块时,首先会检查锁的标志位。默认情况对的标志
位置
1,此时线程会执行同步代码块,同时将锁对象的标志位为0.当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后锁对象的标志位被置位1,新线程才能进入同步代码块其中的代码。循环往复,直到共享资源被处理完为止。
注意:同步代码块中锁对象可以是任意的对象,但多个线程共享的锁对象必须是唯一的。“任意”说的是共享锁对象的类型。所以,锁对象的创建不能放在run()方法中,否则每一个线程运行到run()方法都会创建一个新的对象,这样每一个线程都会有一个不同的锁,每一个线程都有自己的标志。线程之间便不能产生同步的效果
【5.5.3】同步方法
了解到同步代码块可以有效解决线程问的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加同步锁。在方法前同样可以使用synchronized关键字类修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,
synchronized 返回值类型 方法名([参数1,...])
package com.yancan.ru5;
public class Example04 {
public static void main(String[] args) {
Thcket1 thcket1 = new Thcket1();
new Thread(thcket1,"线程一").start();
new Thread(thcket1,"线程二").start();
new Thread(thcket1,"线程三").start();
new Thread(thcket1,"线程四").start();
}
}
class Thcket1 implements Runnable{
private int number = 10;
public void run() {
while(true){
SaleTicket();
if(number<=0){
break;
}
}
}
private synchronized void SaleTicket(){
if(number > 0){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"------"+number--);
}
}
}
被修饰的方法在某一个时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行方法
【5.5.4】死锁问题
package com.yancan.run6;
public class Example {
public static void main(String[] args) {
DeadLockThread deadLockThread = new DeadLockThread(true);
DeadLockThread deadLockThread2 = new DeadLockThread(false);
new Thread(deadLockThread,"yi").start();
new Thread(deadLockThread2,"er").start();
}
}
class DeadLockThread implements Runnable{
static Object chopsticks = new Object();
static Object knifeAndFork = new Object();
private boolean flag;
public DeadLockThread(boolean flag) {
this.flag = flag;
}
public void run() {
if(flag){
while (true){
synchronized (chopsticks) {
System.out.println(Thread.currentThread().getName()+"---if---chopsticks");
synchronized (knifeAndFork) {
System.out.println(Thread.currentThread().getName()+"---if---knifeAndFork");
}
}
}
}else{
while(true){
synchronized (knifeAndFork) {
System.out.println(Thread.currentThread().getName()+"---else---knifeAndFork");
synchronized (chopsticks) {
System.out.println(Thread.currentThread().getName()+"---else---chopsticks");
}
}
}
}
}
}
创建了一个Chinese和American两个线程,分别执行run()方法中if和else代码快中的同步代码块。Chinese线程中用用
有
chopsticks锁,只有获得KnifeAndFork
锁才能执行完毕,而American线程拥有KnifeAndFork锁,只有获得chopsticks锁才能执行完毕,两个线程都需要对方占用的锁,但是都无法释放自己拥有的锁,于是这两个线程都处于挂起状态
这主要理解为什么要有同步代码块,代码块主要功能是干嘛,用什么来区别锁
synchronize(lock){
操作共享资源代码块
}
这里的lock是锁,锁创建任意对象,而且这个对象只会有一个。
Object object = new Object();
object 这就是把锁
object
有且只能有一个
而同步方法,它的锁就是this调用该方法的对象
多线程通信
下面三个方法用了解决线程之间的通信
下面提供了一个链接,这个了解的更加详细
3个与线程通信相关的方法,其中wait()方法用于使当前线程进入等待状态,notify()和notifyAll()方法用于唤醒当前处于等待状态的线程。需要注意的是,wait(),notify(0,notifyAll()这三个方法的调用者都应该是
同步锁对象,如果这三个方法的调用者不是同步锁对象,Java虚拟机就会抛出IllegalMonitorStatExample异常。
package com.yancan.run7;
import java.awt.Image;
public class Example01 {
public static void main(String[] args) {
Storage st = new Storage();
Input input = new Input(st);
Output output = new Output(st);
new Thread(input, "存入").start();
new Thread(output, "取出").start();
}
}
/*
* class Storage { private int[] cells = new int[10]; //
* inPOS表示存入数字下标,outPOS表示取出来的下标 private int inPos = 0; private int outPos = 0;
*
* // 定义一个put()方法向数字中存入数据 public void put(int num) { cells[inPos] = num;
* System.out.println("在cells[" + inPos + "]中存放数据---" + cells[inPos]); inPos++;
* if (inPos == cells.length) { inPos = 0; } }
*
* public void out() { int data = cells[outPos]; System.out.println("在clees[" +
* outPos + "]取出数据---" + data); outPos++; if (outPos == cells.length) { outPos =
* 0; } } }
*/
class Storage {
private int[] cells = new int[10];
// inPOS表示存入数字下标,outPOS表示取出来的下标
private int inPos = 0;
private int outPos = 0;
private int count;
public synchronized void put(int num) {
try {
// 如果放入数据等于cells的长度,就在此等待
while (count == cells.length) {
this.wait();
}
cells[inPos] = num;
System.out.println("在cells[" + inPos + "]中存放数据---" + cells[inPos]);
inPos++;
if (inPos == cells.length) {
inPos = 0;
}
count++;
this.notify(); //每次调用的时候都会唤醒一次
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void out(){
try {
while(count == 0){
this.wait();
}
int data = cells[outPos];
System.out.println("在clees["+ outPos + "]取出数据---" + data);
cells[outPos] = 0;
outPos++;
if(outPos == cells.length){
outPos = 0;
}
count--;
this.notify();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class Input implements Runnable {
private Storage st;
private int num = 0;
Input(Storage st) {
this.st = st;
}
public void run() {
while (true) {
st.put(num++);
}
}
}
class Output implements Runnable {
private Storage st;
Output(Storage st) {
this.st = st;
}
public void run() {
while (true) {
st.out();
}
}
}
首先通过使用synchronized关键字将put()方法和get()方式修饰为同步方法,之后每操作一次数据,便调用一次notify()方法唤醒对应同步锁上等待结果。当存入数据时,如果count的值和cells数组相等的时候进入等待状态。同理,当取出数据时,如果count的值为0,说明数组以被去空,此时就需要调用同步锁的wait()方法,使取出数据的线程进入等待状态。