一、线程的生命周期
新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件
运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态,run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性的中止
二、创建线程的方式
1、继承于Thread类
class PrintNum extends Thread{
public void run(){
//子线程执行的代码
for(int i = 1;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public PrintNum(String name){
super(name);
}
}
public class TestThread {
public static void main(String[] args) {
PrintNum p1 = new PrintNum("线程1");
PrintNum p2 = new PrintNum("线程2");//开启几个线程就需要几个对象
p1.setPriority(Thread.MAX_PRIORITY);//10
p2.setPriority(Thread.MIN_PRIORITY);//1
p1.start();
p2.start();
}
}
//匿名对象
new Thread(){
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
2、实现Runnable接口
class SubThread implements Runnable{
public void run(){
//子线程执行的代码
for(int i = 1;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class TestThread{
public static void main(String[] args){
SubThread s = new SubThread();//只需要一个实例对象
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
//匿名子线程
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("分线程执行的操作");
}
}).start();
Tip
:
1️⃣一个线程只能调用一次start()方法,但可以创建多个子线程类的对象,每个对象分别调用start()方法是可以的
2️⃣多线程可看作是多个子线程的run()方法和main()方法交互执行,争夺cpu资源的过程,子线程对象不能直接调用run()方法,若直接调用run()方法的话相当于没有启动子线程,此时的run()方法和普通方法没什么两样,程序会按顺序执行完run()方法再执行其他语句,只有调用了子线程对象的start()方法才能启动该子线程
3、实现Callable接口
public class CallerTask implements Callable<String>{
@Override
public String call() throws Exception{
return "Hello future!";
}
public static void main(String[] args){
FutureTask<String> task = new FutureTask<>(new CallerTask());
new Thread(task).start();
try{
String result = task.get();
System.out.println(result);
}catch(ExecutionException | InterruptedException e){
e.printStackTrace();
}
}
}
这种方式可以获取线程执行的结果。
4、两种方式的对比
联系:class Thread implements Runnable,Thread类本身实现了Runnable接口
实现的方式较好:
①解决了单继承的局限性
②如果多个线程需要共享数据的话,建议使用实现的方式(继承的方式因为要创建多个对象来启动线程,就必须将共享数据声明为static的),同时,共享数据所在的类可以作为Runnable接口的实现类。
当然,创建多线程可以同时继承Thread和实现Runnable,看似有些不伦不类,此时无需再专门借助Thread类的对象的start()方法来启动线程,而直接使用子线程类对象的start()方法即可,另外在有共享数据时是可能发生线程安全问题的,这时需考虑同步:
public class ThreadTest {
public static void main(String[] args) {
SubThread t1 = new SubThread();
SubThread t2 = new SubThread();
SubThread t3 = new SubThread();
t1.setName("线程1");
t2.setName("线程2");
t3.setName("线程3");
t1.start();
t2.start();
t3.start();
}
}
//子线程类即继承Thread又实现Runnable
class SubThread extends Thread implements Runnable {
private static int ticket = 100;
@Override
public void run() {//该run方法是对Thread类中run方法的重写
while (ticket > 0)
synchronized (SubThread.class) {
if (ticket > 0)
System.out.println(Thread.currentThread().getName() + ":" + ticket--);
}
}
}
三、多线程常用的方法
1、start():开启线程;执行相应的run()
2、run():分线程要执行的逻辑声明在run()中
3、currentThread():获取当前正在执行的线程
4、getName():获取线程的名字
5、setName():设置线程的名字
6、yield():释放线程对当前cpu的占用,但不是放锁
7、join():在线程a中调用b.join()表示当前线程a进入阻塞状态,线程b开始执行,直到线程b执行结束,线程a才继续执行
8、sleep(long millitimes):显示的使当前线程睡眠指定的毫秒数,这个过程中,当前线程处于阻塞状态,不会释放锁
9、isAlive():当前线程是否还存活
10、线程的优先级:getPriority() / setPriority(int i),值越大优先级越高,提供的值有MIN_PRIORITY(值为1)、NORM_PRIORITY(值为5)、MAX_PRIORITY(值为10),同优先级线程组成先进先出队列,高优先级优先调度,使用时间片策略;线程创建时继承父线程的优先级。优先级设置的越高,该线程越容易抢到CPU资源,但并不意味着一定先执行。
11、wait():会释放锁
四、线程的同步
如果我们创建的多个线程,存在着共享数据,那么就有可能出现线程的安全问题:当其中的一个线程操作共享数据,在还未操作完成时,另外的线程就参与进来,导致对共享数据的操作出现问题。
解决方式:要求一个线程操作共享数据时,只有当其完成操作,其它线程才有机会操作共享数据,也就是说共享数据最多只能有一个线程在操作
线程同步的方式:
1、同步代码块
synchronized(同步监视器){
//操作共享数据的代码
}
单例模式线程安全问题的解决:
class Singleton {
private Singleton() {
// 私有化构造器
}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {// 提高了效率,外面的进程不用再等待了
synchronized (Singleton.class) {// 当前类充当锁
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
注
:
1️⃣同步监视器:俗称锁,任何一个类的对象都可以充当锁。要想保证线程的安全,必须要求所有的线程共用同一把锁;
2️⃣使用实现Runnable接口的方式创建多线程的话,同步代码块中的锁,可以考虑使用this,因为这时可以只创建一个线程对象。如果使用继承Thread类的方式,则慎用this;
3️⃣明确哪部分是操作共享数据的代码,理论上说同步代码块越小越好
2、同步方法:将操作共享数据的方法声明为synchronized
public synchronized void show(){
//操作共享数据的代码
}
注
:
1️⃣对于非静态的方法,同步方法的锁默认为this,即当前对象,因此需要保证对象时单例的。如果使用继承的方式实现多线程的话,慎用同步方法;
2️⃣对于静态的方法,如果使用同步,默认的锁为当前类本身
;
3️⃣线程的死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁;
3、在线程安全控制中,通常使用ReentrantLock(可重入锁)。使用该Lock对象可以显示加锁、释放锁:
class C{
//锁对象:注意是static final的,保证唯一
private static final ReentrantLock lock = new ReentrantLock();
......
//保证线程安全方法
public void method() {
//上锁
lock.lock();
try {
//保证线程安全操作代码
} catch() {
} finally {
lock.unlock();//释放锁
}
}
}
使用Lock对象进行同步时,注意把释放锁的操作放在finally中,保证一定能够执行。使用锁和使用同步类似,只是使用Lock是显示的调用lock方法来完成同步。而使用synchronized时系统会隐式使用当前对象作为同步监视器,同样都是“加锁->访问->释放锁”的操作模式,都可以保证只有一个线程在操作资源。同步方法和同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且获得多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有资源。Lock提供了同步方法和同步代码块没有的其他功能,包括用于非块结构的tryLock方法,以及试图获取可中断锁lockInterruptibly()方法,还有获取超时失效锁的tryLock(long, timeUnit)方法。
ReentrantLock具有重入性,也就是说线程可以对它已经加锁的ReentrantLock再次加锁,ReentrantLock对象会维持一个计数器来追踪lock方法的嵌套调用,线程在每次调用lock()加锁后,必须显示的调用unlock()来释放锁,所以一段被保护的代码可以调用另一个被相同锁保护的方法。
五、同步示例
1、两人同向一个账户存钱,每次存1000,共存三次
①采用继承的方式
public class Bank {
public static void main(String args[]) {
Account acct = new Account();// 一个账户
Customer cust1 = new Customer();
Customer cust2 = new Customer();
cust1.setAccount(acct);
cust2.setAccount(acct);// 两人共有
cust1.start();// 开启线程开始存钱
cust2.start();
}
}
// 账户
class Account {
private double balance;// 两人共享同一个账户,实际上就是共享余额这个数据
public double getBalance() {
return this.balance;
}
public synchronized void depsit(double amt) {// 对余额的操作要同步
this.balance += amt;
System.out.println(Thread.currentThread().getName() + ":" + balance);
}
}
//采用继承的方式
class Customer extends Thread {
private Account account;
public void setAccount(Account account) {
this.account = account;
}
//存钱
public void run() {
for (int i = 0; i < 3; i++) {
account.depsit(1000);
}
}
}
②采用实现的方式
// 实现的方式
public class Bank {
public static void main(String[] args) {
Account acct = new Account();
Customer cust = new Customer(acct);
Thread t1 = new Thread(cust);
Thread t2 = new Thread(cust);
t1.start();
t2.start();
}
}
class Account {
private double balance;
public synchronized void deposit(double amt) {
this.balance += amt;
System.out.println(Thread.currentThread().getName() + ":" + balance);
}
}
class Customer implements Runnable {
private Account account;
public Customer(Account acct) {
this.account = acct;
}
public void run() {
for (int i = 0; i < 3; i++) {
account.deposit(1000);
}
}
}
2、生产者和消费者模型
public class ProducerCusumer {
public static void main(String[] args) {
Production pro = new Production();// 产品
Producer p = new Producer(pro);// 生产者
Consumer c = new Consumer(pro);// 消费者
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
t1.setName("生产者");
t2.setName("消费");
t1.start();
t2.start();
}
}
class Producer implements Runnable {
private Production production;
public Producer(Production pro) {
this.production = pro;
}
public void run() {
while (true) {
production.producePro();
}
}
}
class Consumer implements Runnable {
private Production production;
public Consumer(Production pro) {
this.production = pro;
}
public void run() {
while (true) {
production.consumePro();
}
}
}
class Production {
private int num;
public synchronized void producePro() {
if (num < 20) {
try {
Thread.currentThread().sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
num++;
notifyAll();// 只要生产了一个就可以唤醒消费者了
System.out.println(Thread.currentThread().getName() + "目前生产是的第" + num + "个商品。");
} else {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public synchronized void consumePro() {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "目前消费是的第" + num + "个商品。");
try {
Thread.currentThread().sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
num--;
notifyAll();// 只要消费了一个就可以唤醒生产者了
} else {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}