1,进程定义:进程就是指正在执行的程序,怎样查看正在执行的进程呢?我们在使用电脑的时候,其实就有多个正在执行的程序,通过Ctri+Alt+Del 组合键可以进入windows任务管理器查看进程,我们进入后会看到很多.exe,这些就是我们的电脑当前正在执行的程序,也就是一个个的进程。
每一个程序执行的都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。
2,线程:是进程中一个独立控制单元,控制着进程的执行。一个进程中至少有一个线程。我们之前写的程序都是单线程的程序。我们在编译java文件的时候,会启动javac进程,启动java命令时,会启动JVM执行.class文件。这个进程中至少有一个负责线程的执行,这个线程运行的代码就是main函数里面的代码,该线程称之为主线程。
注意:通常我们可以理解为这样的程序时单线程程序,实际上并不止就这一个线程。原因是还有一个线程就是JVM的垃圾回收机制中控制垃圾回收的线程,在主线程执行的时候会启动,回收内存中不再使用的对象的内存,其实最少有两个线程。
多线程最常见的应用就是下载软件,下载软件在下载东西的时候,就是多个线程同时向服务器发送请求,同时多条路劲在下载文件。其实多线程在执行的时候并不是多个线程同时执行,而是CPU在多个线程之间进行这快速的切换,中间的事件间隔我们可以忽略,因为太快了,所以我们认为是同时在执行,其实这种执行叫做并发执行。
二,线程的五种状态:
(1)被创建:创建Thread类的子类,将要运行的代码放在run方法中,调用start方法创建线程,并调用run方法,此时线程进入运行状态。
(2)运行:运行状态就是run方法中的代码执行过程,调用stop方法终止整个线程,run方法结束。运行状态时调用sleep方法或者wait方法,是线程进入(3)冻结状态,此时线程放弃了执行权,当睡眠时间或者从冻结状态调用notify方法,能从冻结状态转化为运行状态。
(4)阻塞:这个状态比较特殊,这个状态线程具有执行权,但是在等待CPU资源,这个状态有可能在run中的代码运行一部分还没运行完时,CPU去执行其他线程中的代码去了。冻结状态被叫醒后不一定直接进入运行状态,也有可能进入阻塞状态。当然阻塞状态也有可能进入冻结状态。冻结状态:没有了执行权,当然某个线程睡眠或者等待的时候。
(5)消亡:也就是该线程结束,run中的代码执行完。如果中途要关闭,则通过调用stop方法,否则线程执行完自动消亡。
三,自定义线程:
参考java文档的Thread类时,发现:
创建新执行线程有两种方法。一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。例如,计算大于某一规定值的质数的线程可以写成:
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
然后,下列代码会创建并启动一个线程:
PrimeThread p = new PrimeThread(143);
p.start();
通过API的解释可以看出创建一个线程的方式一继承Thread类:
1,创建一个类,继承Thread类
2,重写Thread类中的run方法
3,调用线程的启动方法,start,该方法的作用有两个,一个是启动线程和调用run方法。
示例一:
class RunDemo extends Thread {
public void run() {
for(int i=0;i<70;i++) {
System.out.println("Demo run ......");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
RunDemo d = new RunDemo();
d.start();
for(int i=0;i<70;i++) {
System.out.println("main run...");
}
}
}
这个程序在执行的时候应该是Demo run和main run是交替执行的。执行过程是主线程启动,main函数执行,然后RunDemo线程启动,run方法和main方法里面的for循环并发执行。
4,为什么定义一个继承Thread类的类的线程时候,要调用start方法,而不调用run方法呢?原因是如果调用了run方法,那么就不是一个独立的控制单元控制一段代码块的执行了,就成了方法的调用,程序中相当于只有一个main线程。程序会按顺序执行。
示例二:定义一个线程类,然后在main方法中启动两个自定义线程,交替执行线程中的内容。
class RunDemo extends Thread {
//private String name;
RunDemo(String name) {
//this.name = name;
super(name);//调用父类的构造函数给自定义线程赋一个名字
}
public void run() {
for(int i=0;i<70;i++) {
System.out.println(Thread.currentThread().getName() + " run ......" + i);
//System.out.println(this.getName() + " run ......" + i);等价于上面这个
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
RunDemo d = new RunDemo("one");
RunDemo d1 = new RunDemo("two");
d.start();
d1.start();
/*for(int i=0;i<70;i++) {
System.out.println("main run...");
}*/
}
}
Thread.currentThread().getName()可以获得线程对象的名字,通过setName或者构造函数可以设置名字,其他操作查看java文档。
自定义线程方式二:
创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。采用这种风格的同一个例子如下所示:
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
然后,下列代码会创建并启动一个线程:
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
实现Runnable接口的方式创建线程步骤:
1,定义一个类实现Runnable接口;
2,覆盖Runnable接口中的run方法;目的:将线程要运行的代码存放在该run方法中;
3,通过Thread类建立线程对象;
4,将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数;原因是:run方法是属于Runnable接口的子类对象,所以要让线程指定所指定对象的run方法,就必须明确run方法所属的对象;
5,调用Thread类的run方法,启动线程,并调用Runnable接口子类的run方法。
示例:
需求:模拟火车站窗口购票系统,一共100张票;
分析:利用多线程的原理,火车票多个窗口同时在卖一定数量的火车票,当窗口1卖了1号座位的车票,其他窗口就不能再卖1号座位的票了;那个窗口卖的是几号座位的票取决于cpu的执行顺序,多个窗口卖火车票,相当于多个线程在同时执行;如果使用方式一创建线程必定出问题,假设四个窗口,每个窗口都要创建一个Thread子类的对象,这样一共就是400张票。如果只创建一个对象,让该对象运行四次,那么运行时必定会出现错误提示,线程状态错误。所以使用这种方式创建时不可以的。解决方法是使用第二种创建线程的方式:
代码如下:
class Demon2 implements Runnable {
private int tickets = 100;
Object obj = new Object();
public void run() {
while(true) {
if(tickets>0) {
System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);
}
}
}
}
class ThreadTest2 {
public static void main(String[] args) {
//创建Runnable接口子类对象
Demon2 d = new Demon2();
//创建线程对象,将Runnable接口的子类对象传给线程
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
Thread t3 = new Thread(d);
Thread t4 = new Thread(d);
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
总结:实现方式和继承方式的区别:
继承Thread,线程代码存放在Tread子类的的run方法中。
实现Runnable,线程代码存放在接口的子类的run方法中。
实现的好处:避免了单线程的局限性。定义线程的时候,第一种方式不建议使用。建议使用第二种方式。
四,线程的同步:
1,上述卖票系统,在判断tickets之后让该线程睡眠1秒钟,这时候通过分析发现会打印出0,-1,-2,-3等错误座位,这就是多线程存在的安全问题。原因:当多条语句在操作同一个线程共享数据时,一个线程的多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误;
解决方法:
对多条操作共享数据的语句,只能让一个线程执行完,在执行过程中,其他线程不可以参与进来执行;
2,Java对多线程的安全问题有专门的解决方法:就是同步代码块;
synchronized(对象) {
需要被同步的代码块
}
对象如同锁,持有锁的线程可以在同步中执行。没有持有锁的线程即使获得cpu的执行权,也进不去,因为没有锁;哪些代码需要同步,就看哪些语句在操作共享数据;
3,同步的前提是:
(1)要有两个或两个以上的线程;(2)必须是多个线程使用同一个锁;(3)必须保证同步中只能有一个线程在执行;
请看下面示例:
class Demon2 implements Runnable {
private int tickets = 100;
Object obj = new Object();
public void run() {
while(true) {
synchronized(obj) { //重点部分,加锁了。每个线程进来之前都会判断该锁是否开启,
//如果开启就进入,然后将锁关闭,这样后来的线程就没法进入,等之前进来的程序执行完后才能进来
if(tickets > 0) {
try{Thread.sleep(10);}catch(Exception e) {}
System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);
}
}
}
}
}
class ThreadTest2 {
public static void main(String[] args) {
Demon2 d = new Demon2();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
}
}
4,在函数上加锁:
虽然锁的方法有两种,就是锁住共享数据部分或者锁住函数,但是这里如果直接给函数上锁的话,一旦一个线程进去之后就不能出来,所以不能直接在run函数上加锁,要先将共享数据封装到一个函数内部,然后多该封装函数加锁。
class Demon2 implements Runnable {
private int tickets = 1000;
//Object obj = new Object();
boolean flag = true;
public void run() {
if(flag) {
while(true) {
synchronized(this) { //如果改成自定义的obj,那么这两个进程使用的锁就不是同一个锁,不满足同步的条件
if(tickets > 0) {
try{Thread.sleep(10);}catch(Exception e) {}
System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);
}
}
}
}
else
while(true){
show();
}
}
public synchronized void show() {//这里的锁使用的对象时this
if(tickets > 0) {
try{Thread.sleep(10);}catch(Exception e) {}
System.out.println(Thread.currentThread() + "print ticket no:" + tickets --);
}
}
}
class ThreadTest2 {
public static void main(String[] args) {
Demon2 d = new Demon2();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t1.flag = flase;
t2.start();
}
}
五,结合线程同步的单例设计模式。
//饿汉式
class Single {
private static final Single s = new Single();
private Single() {}
public static Single getInstance() {
return s;
}
}
//带延迟加载的懒汉式
class Single2 {
private static Single2 s = null;
private Single2() {}
public static Single2 getInstance() {
if(s == null) {
synchronized (Single2.class) {
if(s == null) {
s = new Single2();
}
}
}
return s;
}
}
重点:面试中可能会问到:懒汉式和饿汉式的区别是什么?回答:懒汉式的特点在于延迟加载。懒汉式的延迟加载有没有什么问题?回答:如果是多线程访问时会出现安全问题。解决方法是用同步来解决。用同步代码块和同步函数都可以,但是效率比较低。用双重判断的方式能够解决效率问题。同步的时候的锁是属于该类所属的字节码文件对象。
六,死锁。
所谓的死锁就是指,两个进程各自拿着各自的锁而不是放资源,而每个线程要想运行,就必须拿到对方的锁,这时候就会出现死锁的问题。
关于死锁的示例:
class ThreadDead implements Runnable {
private boolean flag;
public ThreadDead(boolean flag) {
this.flag = flag;
}
public void run() {
while(true) {
if(flag) {
synchronized (MyLock.lock1) {
System.out.println("if lock1 run...");
synchronized (MyLock.lock2) {
System.out.println("if lock2 run...");
}
}
}
else {
synchronized (MyLock.lock2) {
System.out.println("else lock2 run...");
synchronized (MyLock.lock1) {
System.out.println("else lock1 run...");
}
}
}
}
}
}
class MyLock {
static Object lock1 = new Object();
static Object lock2 = new Object();
}
public class DeadLock {
public static void main(String[] args) {
Thread t1 = new Thread(new ThreadDead(true));
Thread t2 = new Thread(new ThreadDead(false));
t1.start();
t2.start();
}
}