支持
java对于多线程的支持在类Thread 与接口Runnable(Callable)中体现
Thread类
Thread类是一个支持多线程的功能类,只要有一个子类,他就可以实现多线程的支持
所有程序的起点是Main方法,所有的线程也一定要有一个自己的起点,即run方法
在多线程的每个主体类之中,都必须复写run方法
线程与进程相同,都必须抢占资源,只有多个线程交替进行,才是多线程,而多线程启动方法为start方法,而调用start方法则执行的方法体是start定义的
为何Thread类启动是调用start方法
因为该方法在Thread类里面存在着一个IllegalThreadStateException异常抛出,即若某一个线程对象重复进行了启动,那么就会抛出此异常
发现在start方法里调用了一个start0方法,而且此方法的结构与抽象方法中用了JNI(Java Native Interface)技术,这门技术的特点是使用Java调用本机操作系统提供的函数。但是这样的技术也有一个缺点,即不能够离开特定的操作系统
如果要想线程能够执行,需要操作系统来进行资源分配,所以此操作严格来讲主要是由JVM负责根据不同的操作系统而实现的。
即:使用Thread类的start方法不仅仅要启动多线程的执行代码,还要去根据不同的操作系统,进行资源的分配。
实现Runnable接口
虽然Thread类可以实现多线程的主体类定义,但是他有一个问题,Java具有单继承的局限性,正因为如此,在任何情况下我们针对于类的继承都应该是回避的问题,那么多线程也一样,为了解决单继承的限制,在Java里面,专门提供了Runnable接口
在结构上,继承Threand类与实现Runnable接口的类是没有区别的,但是如果类是继承了Thread类,那么可以直接继承start方法,但如果实现的是Runnable接口,则并没与start方法可以被继承
不管何种情况下,如果要想启动多线程,一定依靠Thread类来完成.
Thread类有接受Runnable对象的构造方法,即我们可以依靠以下代码实现
MyThread2 a = new MyThread2("a");
MyThread2 b = new MyThread2("b");
MyThread2 c = new MyThread2("c");
new Thread(a).start();
new Thread(b).start();
new Thread(c).start();
两种实现方式的区别
- 使用Runnable接口与Thread类解决了单继承的局限性
- 在多个线程访问同一资源的情况下,Runnable比Thread更好
Runnable实现多个线程实现数据共享时可以通过启动多个线程但构造自一个主体来实现,即MyThread类实现了Runnable 接口,然后以如下代码的方式实现
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
- Thread类是Runnable接口的子类
- Runnable接口实现的多线程,比Thread类实现的多线程更加清楚的描述数据共享的概念
线程的命名去取得
- 构造方法,public Thread(Runnable target ,String name)
- setName方法
对于线程方法发现一个问题,这些方法是属于Thread类里面的,可是如果换回到Runnable子类中,并没有继承Thread子类。所以如果要取得线程名字,那么能够取得的就是当前执行本方法的线程名字。所以在THread类中有一个取得当前线程对象。
如果在我们实例化Thread类对象的时候,没有为其设置名字,那么会进行自动的命名,保证Thread类名字不重复
主方法也同样是一个线程,叫做主线程(main线程) ,所有在主方法上创建的线程都可以将其表示为子线程
每当使用Java命令去解释一个程序类的时候,对于操作系统而言,都相当于启动了一个进程中的一个子线程
每一个JVM进程启动的时候至少启动了
- main线程: 程序的主要执行以及启动子线程
- gc线程: 负责垃圾回收
线程休眠
线程休眠即Thread.sleep方法,值得注意的是,当有多个线程同时进入run方法时,考虑到资源抢占问题,即使是一起休眠一起唤醒,则每次抢占资源的先后顺序有着差别
线程优先级
所谓的优先级指的是越高的优先级越有可能先执行。在Thread类里面提供以下方法进行优先级别操作
- 设置优先级 public final void setPriority(int newPriority)
- 取得优先级 public final int getPriority()
对于设置优先级,有三种取值:MAX_PRIORITY NORM_PRIORITY MIN_PRIORITY
注意:优先级不是绝对的,理论上优先级越高,越有可能先执行,即存在优先级低的线程执行在优先级高的线程之前
注意: 主线程属于中等优先级
线程的同步产生原因
实际上所谓的同步指的就是多个线程访问同一资源时所需要考虑到的问题
假设线程类如下
public class MyThread2 implements Runnable {
private int num =10;
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<200;i++){
if(this.num >0){
System.out.println("相减,num = "+ this.num--);
}
}
}
}
主类如下
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread2 mt = new MyThread2();
new Thread(mt,"A").start();
new Thread(mt,"B").start();
new Thread(mt,"C").start();
}
}
运行结果下发现,三个线程对于一个数据没有出现问题,但是这原因是因为是在一个JVM进程下运行,并且没有受到任何影响,如果想要观察到问题,可以加入一个延迟,即在run方法中加入sleep方法。此时进行操作以后发现出现了不同步的问题。
分析这一现象,即每个线程在for循环语句时,首先要判断条件,其次修改数值,在第一种情况下,线程进入条件以后立刻修改数值,在第二种情况下,线程进入条件以后,进入休眠状态,此时第二个线程同样进入条件,因为第一个条件进入条件后并未立刻修改数值,所以出现了不同步的现象。
同步操作
通过观察可以发现以上程序所带来的最大问题是:在判断与修改的操作是分开执行的
在java里实现线程的同步可以使用synchronized关键字,而这个关键字可以使用两种方式使用
- 同步代码块,即同步块
- 同步方法
方法一:
public class MyThread2 implements Runnable {
private int num =10;
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<200;i++){
// 当前操作每次只允许一个对象进入
synchronized(this){
if(this.num >0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("相减,num = "+ this.num--);
}
}
}
}
}
方法二:
public class MyThread2 implements Runnable {
private int num =400;
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<500;i++){
this.minus();
}
}
public synchronized void minus(){
if(this.num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" ,"+this.num --);
}
}
}
注意: 多个线程访问同一资源时一定要同步,但异步操作的执行速度高于同步操作,但同步操作时数据的安全性较高,属于线程安全的线程操作
死锁
通过分析可以发现,所谓的同步指的就是一个线程对象等待另一个线程对象执行操作完毕的操作形式。通过以下例子,可以发现A类的操作在等待B,B类的操作在等待A,即形成了死锁。
class A{
public synchronized void set (B b){
System.out.println("A: need B");
b.get();
}
public synchronized void get(){
System.out.println("A: get B");
}
}
class B{
public synchronized void set (A a){
System.out.println("B: need A");
a.get();
}
public synchronized void get(){
System.out.println("B: get A");
}
}
public class TestDemo implements Runnable {
private static A a = new A();
private static B b = new B();
public TestDemo(){
new Thread(this).start();
b.set(a);
}
@Override
public void run() {
// TODO Auto-generated method stub
a.set(b);
}
}
注意: 本代码只是为了说明死锁的例子,而没有任何参考性,死锁是由于某种逻辑上的错误所造成的错误。
线程的生命周期
当线程被new时,则进入了创建状态。一旦我们调用了start方法,线程进入就绪状态,线程被加入到线程队列,等待CPU的服务,等到获取到CPU服务时,则进入到了运行状态,开始执行run方法里面的逻辑,线程的run方法执行完毕,或者线程调用了stop方法,线程便进入了终止状态。
(stop方法如今已经被淘汰了),当线程运行时遇到阻塞事件时,会进入阻塞状态。
阻塞事件是指,一个正在执行的线程在某些情况下或者某种原因,暂时让出了CPU资源,暂停了自己的执行,进入到了阻塞状态,如调用了sleep方法。
等到阻塞状态解除,就会重新进入到了就绪状态。
守护线程
Java线程共分两类,
- 用户线程,运行在前台,执行具体的任务。程序的主线程,连接网络的子线程都是用户线程。
- 守护线程 运行在后台,为其他前台线程服务,一旦所有的用户线程都结束运行,守护线程会随JVM一起结束工作。
守护线程的应用: 数据库连接池的监测线程,JVM虚拟机启动后的监测线程
设置守护线程
可以调用Thread类的setDaemon(true) 方法来设置当前的线程为守护线程
注意事项:
- setDaemon必须在start方法之前调用,否则会抛出异常
- 守护线程中产生的新线程也是守护线程
- 不是所有的任务都可以分配给守护线程来执行,比如读写操作和计算逻辑
当守护线程中有读写操作时,在所有用户线程结束后,守护线程也会自行结束,若此时读写操作未完成,便会引起错误 ,以下为代码示例
class DaemonThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("进入守护线程"+Thread.currentThread().getName());
try{
writetoFile();
}catch(Exception e){
e.printStackTrace();
}
System.out.println("退出守护线程"+Thread.currentThread().getName());
}
private void writetoFile() throws IOException {
File file = new File("daemon.txt");
if(!file.exists()){
file.createNewFile();
}
OutputStream out = new FileOutputStream(file,true);
int count = 0;
while(count<999){
out.write(("\r\nword"+count ).getBytes());
System.out.println("守护线程"+Thread.currentThread().getName()+"向文件中写入了word"+(count++));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("程序进入了主线程"+Thread.currentThread().getName());
DaemonThread daemonThread = new DaemonThread();
Thread thread = new Thread(daemonThread);
thread.setDaemon(true);
thread.start();
//阻塞操作
Scanner sc = new Scanner(System.in);
sc.next();
System.out.println("程序退出了主线程"+Thread.currentThread().getName());
}
}
线程退出的方法
在Thread类方法中, stop方法是停止线程的方法之一。但是如今stop方法已经是属于历史遗留代码,而不推荐在新的代码中使用,因为stop方法会使线程突然停止而导致无法进行后续的清理工作。
停止的线程的方法之一是使用退出标志,这里布尔变量一定要加上volatile的修饰符,保证其线程正确的读取值。
volatile boolean keeprunning = true;
public void run(){
while(keeprunning){
DO_SOMETHING
}
}
关于interruput()方法
在JavaAPI中,对interrupt的描述是中断线程