一、初期理解
1.进程
是一个正在执行中的程序。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。
2.线程
就是进程中的一个独立的控制单元,它在控制着进程的执行,一个进程中至少有一个线程。
3.JVM启动时的线程情况
JVM启动的时候会有一个进程java.exe。该进程中至少一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,称之为主线程。
扩展:其实jvm启动不止一个线程,还有负责垃圾回收机制的线程。
二、多线程
1.方法
setPriority(int newPriority) 更改线程的优先级。
start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
yield() 暂停当前正在执行的线程对象,并执行其他线程。
2.如何在自定义的代码中,自定义一个线程呢?
通过对API的查找,java已经提供了对线程这类事物的描述。如:Thread类。
创建线程的第一种方式:继承Thread类。
步骤:
(1)定义类继承Thread。
(2)复写Thread类中的run方法。目的是将自定义代码存储在run方法,让线程运行。
(3)调用线程的start方法。该方法两个作用:启动线程,调用run方法。
示例:
//定义一个类继承Thread
class Demo extends Thread
{
public void run()//复写run方法
{
for(int x=0; x<60; x++)
System.out.println("demo run----"+x);
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Demo d = new Demo();//创建好一个线程。
d.start();//开启线程并执行该线程的run方法。
//d.run();//此方法仅仅是对象调用方法。而线程创建了,并没有运行。
for(int x=0; x<60; x++)
System.out.println("Hello World!--"+x);
}
}
创建线程的第二种方式:实现Runable接口
步骤:
(1)定义类实现Runnable接口
(2)覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。
(3)通过Thread类建立线程对象。
(4)将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
因为自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去指定指定对象的run方法,就必须明确该run方法所属对象。
(5)调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
示例:
class Ticket implements Runnable//定义类实现Runnable接口
{
private int tick = 100;
public void run()//覆盖Runnable接口中的run方法。
{
while(true)
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);//通过Thread类建立线程对象。将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
t1.start();//调用Thread类的start方法开启线程并调用Runnable接口子类的run方法
t2.start();
}
}
3.实现方式和继承方式有什么区别呢?
继承Thread:线程代码存放Thread子类run方法中。
实现Runnable,线程代码存在接口的子类的run方法。
实现Runnable方法避免了单继承的局限性,在定义线程时,建立使用实现方式。
4.多线程之间怎样运行?
因为多个线程都获取cpu的执行权,cpu执行到谁,谁就运行。
明确一点,在某一个时刻,只能有一个程序在运行(多核除外)。cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。
这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长,cpu说的算。
5.为什么要覆盖run方法?
Thread类用于描述线程。该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。因此要复写run方法。
6.线程对象及名称的获取
线程都有自己默认的名称:Thread-编号;该编号从0开始。
static Thread currentThread():获取当前线程对象。
getName(): 获取线程名称。
setName或者构造函数:设置线程名称。
三、多线程的安全问题
1.问题存在的原因
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程就参与了进来开始执行。导致共享数据的错误。
2.解决方法
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。
Java对于多线程的安全问题提供了专业的解决方式,就是同步代码块。格式如下:
synchronized(对象)
{
需要被同步的代码;
}
对象如同锁。持有锁的线程可以在同步中执行;没有持有锁的线程即使获取cpu的执行权,也进不去。
3.同步代码块
(1)同步的前提
[1]必须要有两个或者两个以上的线程。
[2]必须是多个线程使用同一个锁,因为必须保证同步中只能有一个线程在运行。
(2)同步代码块的优缺点
好处:解决了多线程的安全问题。
弊端:多个线程需要判断锁,较为消耗资源。
4.同步函数
(1)如何找处多线程中寻在的安全问题
[1]明确哪些代码是多线程运行代码。
[2]明确共享数据。
[3]明确多线程运行代码中哪些语句是操作共享数据的。
(2)同步函数
如果整个函数都存在安全问题,可将同步代码块中的synchronized作为修饰词放在函数上,构成同步函数。
(3)同步函数用的是哪一个锁呢?
函数需要被对象调用,那么函数都有一个所属对象引用,就是this。所以同步函数使用的锁是this。
(4)静态同步函数用的锁是什么?
通过验证发现,静态同步函数用的锁不是this,因为静态方法中也不可以定义this。
静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。该对象的类型是Class。
静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class
5.死锁
死锁是一个错误的代码,产生的原因是同步中嵌套同步,它会导致程序一直被锁,等不到运行结果,直至崩溃。开发时一定注意要避免死锁。
示例:
class Ticket implements Runnable{ //创建类实现Runnable接口
private int tick = 10000;
Object obj = new Object();
boolean flag = true; //标记,初始化为true
public void run(){ //复写Runnable的run方法
if(flag){ //如果标记为true,执行同步代码块
while(true){
synchronized(obj){ //加同步代码块,形成嵌套同步--死锁
show(); //调用同步函数show
}
}
}
else //否则执行同步函数
while(true)
show();
}
public synchronized void show(){//同步函数
synchronized(obj){ //同步代码块
if(tick>0){
try{Thread.sleep(1);}catch(Exception e){}
String aa = Thread.currentThread().getName();
System.out.println(aa+".."+tick--);
}
}
}
}
class DeadLockDemo{
public static void main(String[] args){
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(Exception e){}
t.flag = false;
t2.start();
}
}
三、线程间通信--等待唤醒机制
1.定义
其实就是多个线程在操作同一个资源,但是操作的动作不同。
2.方法
wait():等待
notify():唤醒
notifyAll():唤醒线程池中所有线程
这三个方法都使用在同步中。因为要对持有监视器(锁)的线程操作,所以要使用在同步中,只有同步才具有锁。
3.为什么这些操作线程的方法要定义Object类中呢?
因为这些方法在操作同步中线程时,都必须要标识它们所操作线程只有的锁。只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒。不可以对不同锁中的线程进行唤醒,也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以将可以被任意对象调用的方法定义Object类中。
4.wait()和sleep()的区别
wait() 释放资源,释放锁。
sleep() 释放资源,不释放锁。
5.JDK1.5新特性
JDK1.5中提供了多线程升级解决方案。
将同步Synchronized替换成现实Lock操作。将Object中的wait,notify notifyAll,替换成Condition对象。该对象可以Lock锁进行获取。
示例:
import java.util.concurrent.locks.*;
class Resource
{
private String name;
private int count = 1; //编号
private boolean flag = false; //设置标记
private Lock lock = new ReentrantLock();
private Condition condition_set = lock.newCondition();
private Condition condition_out = lock.newCondition();
//生产
public void set(String name)throws InterruptedException {
lock.lock();//获取锁
try{
while(flag)
condition_set.await();
this.name = name+"--"+count++;
String aa = Thread.currentThread().getName();
System.out.println(aa+"..生产者.."+this.name);
flag = true;
condition_out.signalAll(); //唤醒所有消费线程
}
finally{
lock.unlock();//一定要释放锁
}
}
//消费
public void out()throws InterruptedException{
lock.lock();//获取锁
try{
while(!flag)
condition_out.await();
String aa = Thread.currentThread().getName();
System.out.println(aa+".......消费者.."+this.name);
flag = false;
condition_set.signalAll(); //唤醒所有生产线程
}
finally{
lock.unlock();//一定要释放锁
}
}
}
//生产者类实现Runnable接口
class Producer implements Runnable{
private Resource res;
Producer(Resource res){
this.res = res;
}
public void run(){
while(true){
try{
res.set("+商品+");
}catch (InterruptedException e){}
}
}
}
//消费者实现Runnable接口
class Consumer implements Runnable{
private Resource res;
Consumer(Resource res){
this.res = res;
}
public void run(){
while(true){
try{
res.out();
}catch (InterruptedException e){}
}
}
}
class ProducerConsumerDemo2{
public static void main(String[] args){
Resource r = new Resource();
new Thread(new Producer(r)).start();
new Thread(new Producer(r)).start();
new Thread(new Consumer(r)).start();
new Thread(new Consumer(r)).start();
}
}
四、线程扩展
1.停止线程
stop方法已经过时。
(1)如何停止线程?
只有一种,run方法结束。
开启多线程运行,运行代码通常是循环结构。只要控制住循环,就可以让run方法结束,也就是线程结束。
(2)特殊情况:
当线程处于了冻结状态,就不会读取到标记。那么线程就不会结束。
当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除,强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。
Thread类提供该方法 interrupt()来结束线程
2.守护线程
守护线程 setDaemon
特点:开启后和前台线程共同争夺CPU的执行权
public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。
首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
3.join方法
当A线程执行到了B线程的join()方法时,A就会等待,等B线程都执行完,A才会执行。
join可以用来临时加入线程执行。
4.优先级
yield() 暂停当前正在执行的线程对象,并执行其他线程。
setPriority(int newPriority) 更改线程的优先级。newPriority用1-10表示
java已经给出了常用的三个等级:
MAX_PRIORITY 线程可以具有的最高优先级。最高是10
MIN_PRIORITY 线程可以具有的最低优先级。最低是1
NORM_PRIORITY 分配给线程的默认优先级。默认是5