一、进程和线程
1.进程:正在进行的程序
2.线程:是进程中的单个顺序控制流,是一条执行路径。
线程分为单线程和多线程。所谓单线程就是指程序只能从a->b->c->…,不可以在进程中做着a又执行b。所谓多线程就是指进程有两个或两个以上的执行路径,即可同时执行a与b操作
二、实现多线程
1.继承Thread类
继承Thread类(线程类)的类需要重写run()方法。但是直接调用run()方法不会实现多线程。Thread类中提供了一个start()方法,它可以启动线程,并且让JVM调用run()方法,以此达到多线程的目的。
比如说:
public class MyThread extends Thread{
public void run(){
for(int i = 0;i < 100;i ++){
System.out.println(i);
}
}
}
class TestMyThread{
public static void main(String[] args){
MyThread m1 = new MyThread();
MyThread m2 = new MyThread();
//相当于普通方法,无法实现多线程的目的
//m1.run();
//m2.run();
//实现多线程
m1.start();
m2.start();
}
}
为什么要重写run方法?
因为run方法是封装被线程执行的代码。
run方法与start方法区别?
方法 | 作用/特点 |
---|---|
run() | 直接调用相当于一个普通方法 |
start() | 启动线程;使JVM调用run方法,实现多线程目的 |
2.实现Runnable接口
通过创建一个类并且实现接口Runnable,将这个类作为参数传递给Thread类的构造器。
Thread类提供了如下两个构造器:
Thread(Runnable target);
Thread(Runnable target,String name);
使用Runnable接口实现方法的优点:实现了Runnable接口的类可以去继承一个父类,避免了java单继承的局限性。同时较好的体现了OOP思想。
三、设置、获取线程的名字
1.设置线程名字
a.Thread类
Thread t1 = new Thread("marry");
Thread t2 = new Thread();
t2.setName("Maryy" );
System.out.println("t1`s name is " + t1.getName() + " t2`s name is " + t2.getName());
b.MyThread类(自定义线程类)
public class MyThread extends Thread{
public MyThread(){}
public MyThread(String name){
super(name);
}
public static void main(String[] args){
MyThread myThread1 = new MyThread("marry-1");
MyThread myThread2 = new MyThread("marry-2");
MyThread myThread3 = new MyThread();
myThread3.setName("Marry-3");
}
}
两种线程类设置线程名字有些许差别。setName(String name)是在Thread中,因此MyThread可以直接调用。Thread中还存在一个在构造的时候就可以设置线程名称的构造器Thread (String name),MyThread就要使用父类构造器来完成。
2.获取线程名字
getName();
3.获取正在执行线程的对象的引用(说白了就是main方法的线程对象)
Thread.currentThread(),返回的是一个Thread类,如果要知道它的线程名称,则可以使用Thread.currentThread().getName();
四、线程优先级
执行多线程的时候,每一个线程都有一个优先级(Priority)。优先级高的线程可以抢占更大时间比例的CPU时间片,也就是执行这个线程的概率更高。注意:不是线程优先级高就顺序先执行这个高优先级!!!!!!!!
1.设置线程优先级
方法:setPriority(int value)
在设置线程优先级需要注意的是线程的优先级是从1~10,默认优先级是5。不可以超过这个范围,超过这个范围会报错IllegalArgumentException(非法参数错误)。
2.获取线程优先级
方法:getPriority();
五、线程控制
这里介绍三个线程控制的方法
方法名 | 作用 |
---|---|
sleep(long ms) | 控制线程多少ms为一个时间段去抢占cpu时间片 |
join() | 等待这个线程运行完,其余的线程再去抢占cpu |
setDaemon(boolean on) | 设置线程为守护线程,当主线程(main线程)运行结束以后若其余线程全是守护线程,则jvm退出。否则jvm不退出,其余线程继续抢占cpu时间片 |
join()方法要放在start()方法的下一行(或者说是下一个start前) 才发挥作用
六、线程生命周期
七、线程同步
拿卖票问题来讲,假如有三个线程,且具有相同数据ticketNumber=100,每次卖票以后,ticketNumber-1,如果同时启动三个线程,由于线程的随机性,可能三个线程会同时出售同一张票或者是出现ticketNumber<0的情况。这时候引发了线程的数据安全问题。
1.何时会线程出现数据安全问题
a.多线程 b.有公共数据 c.对数据有一条或多条操作
2.利用同步代码块解决数据安全问题
synchronized(任意对象){
代码块
}
需要注意的是,synchronized括号里的任意对象不可以直接在括号里new一个对象。理由:如果是new一个对象的话起不到锁住代码块的作用。因为每一个线程在抢占到了CPU之后,遇到了synchronized代码块以后,都会new一个对象来开锁。所以需要在类的属性中先定义一个类作为开锁对象使用。
3.利用同步方法解决数据安全问题
同步方法的定义:
修饰符 synchronized 返回值类型 方法名(参数列表)
同步方法分为普通同步方法和静态(static)同步方法。在同步方法解决数据安全问题上,如果出现了if···else的结构
if(){
snychronized(对象){
代码A
}//利用了同步代码块的方法
}
else{
代码B
}
两个代码块都有对公共数据的操作,那么这两个代码块都需要上锁。也就是代码A和代码B需要的锁是同一把锁。如果是普通方法,那么锁对应的是this。如果是静态方法,那么锁对应的是这个类的反射(.class)【字节码文件】。
八、线程安全类
1.Vector
与Vector相对应的线程不安全的类ArrayList执行效率更快。
2.StringBuffer
与StringBuffer相对应的线程不安全的类StringBuilder
3.Hashtable
与Hashtable相对应的线程不安全的类HashMap
在不需要线程安全的时候,尽量使用ArrayList、StringBuilder、HashMap这些类,在需要线程安全的时候,除了StringBuffer以外,也不会使用Vector和Hashtable。通常会用Collections.synchronizedList(List<E> list)方法使list转换为线程安全的集合。
九、Lock锁
同步代码块和同步方法解决线程数据安全问题上,我们用了关键字synchronized,需要加上一个锁。这个锁怎么加上的呢?JAVA提供了一个接口Lock。
获取锁: lock()
释放锁:unlock()
Lock是一个接口,无法直接实例化,需要使用一个已经实现了Lock接口的类ReentrantLock进行实例化。
在使用的时候,通常是这样的一个结构
try{
lock.lock();
/*
//这里都是逻辑代码块,可能会调用线程的一些方法
if(a > 100){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("a" + i);
}
*/
}
finally{
lock.unlock();
}
使用try-finally块是因为上锁之后的代码可能会出现异常导致最后无法释放锁。