1、线程的创建
有三种创建方式:
-
继承Thread类
继承之后可重写 run() 方法实现线程体,之后可通过线程对象调用 start() 方法开启线程
public class Myjava extends Thread{ @Override public void run() { //重写run()方法 System.out.println("hello Java"); super.run(); } public static void main(String[] args) throws Exception{ Myjava myjava = new Myjava(); myjava.start(); //开启线程 } }
-
实现Runnable接口
重点是实现 run() 方法实现线程体,之后可以用本类对象为参数创建线程对象并调用 start() 方法开启线程。因为Java只支持单继承,因此推荐使用此方法。
public class Myjava implements Runnable{ @Override public void run() { //实现run()方法 System.out.println("hello java"); } public static void main(String[] args) throws Exception{ Myjava myjava = new Myjava(); new Thread(myjava).start(); //开启线程 } }
-
实现Callable接口
1、实现Callable接口 (需要指定泛型类型)
2、重写call方法(需要抛出异常)
3、创建目标对象与执行服务
4、进行提交并执行
5、获取结果后关闭服务
public class Myjava implements Callable<String> { @Override public String call() throws Exception { return "hello java"; } public static void main(String[] args) throws Exception{ Myjava myjava = new Myjava(); //创建执行服务 ExecutorService executorService = Executors.newFixedThreadPool(2); //提交执行 Future<String> submit = executorService.submit(myjava); //获取结果并结束服务 String result = submit.get(); executorService.shutdownNow(); } }
2、静态代理模式与Lambda表达式
2.1 静态代理模式
代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式,即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作。静态代理使用时,需定义一个接口或父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类。
我们可以通过一个实际的例子来理解一下:我们电视上看的各种节目是由节目制作组提供给电视台播放的,电视台为了盈利还会在播放这些节目时播放各种广告,其中电视台就是代理对象,节目组是被代理对象。可以用代码模拟此过程:
interface Program{ //首先需要一个通用的接口
void Play();
}
class ProgramA implements Program{ //被代理方
@Override
public void Play() {
System.out.println("现在正在播放节目A");
}
}
public class TVstation implements Program{ //TVstation就是代理对象
ProgramA a;
public TVstation(ProgramA a) {
this.a = a;
}
@Override
public void Play() {
System.out.println("现在是广告时间");
a.Play();
}
}
2.2 Lambda表达式
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。使用 Lambda 表达式可以使代码变的更加简洁紧凑。
Lambda表达式有几个特征:
- 可选类型声明:编译器可以识别参数类型
- 可选的参数圆括号:单个参数不需要但多个需要
- 可选大括号:只有一个语句则不需要大括号
- 可选return:主体只有一个表达式则编译器自动返回表达式的值,但当使用大括号时需要显式返回
代码示例:
interface Lambda1{
void lambda();
}
interface Lambda2{
int lambda(int a,int b);
}
public class Myjava{
public static void main(String[] args) throws Exception{
Lambda1 l1=()-> System.out.println("hello");
l1.lambda();
Lambda1 l2=()->{
System.out.println("hello");
System.out.println("Lambda");
};
Lambda2 l3=(a,b)->a+b;
l3.lambda(1,2);
}
}
3、线程方法
3.1 线程状态
共有五大状态,如下图所示:
当Thread对象一经创建就会进入到新生状态,当调用start()方法时立马进入就绪状态但未必会立即调度执行,而当调用sleep()、wait()或者同步锁定时线程就会进入到阻塞状态,代码将不会往下执行当阻塞状态解除后将进入就绪状态等待CPU调度执行。
3.2 线程停止
可以使用JDK提供的stop()或者destroy()方法但不推荐,因为已经被废弃。可以设定一个标志位来让线程自己停下来。示例:
private boolean flag=true;
@Override
public void run() {
while(flag)
System.out.println("hello");
}
public void Mystop(){
flag=false;
}
3.3 线程休眠
通过调用sleep(long)方法可让线程休眠,需要给函数传递一个以毫秒为单位的时间值,在给指定的时间到达后线程将进入就绪状态,此方法是Thread类的一个静态方法。使用此方法可以模拟网络的延时或者倒计时等功能。每个对象都有一把锁,而sleep方法不会释放锁。
使用示例:
Thread.sleep(1000);
3.4 线程礼让与强制执行
调用定义在Thread类中的静态方法yield()可以进行礼让,让线程从运行状态转为就绪状态并让CPU重新进行调度。礼让不一定能成功因为CPU重新调度后可能仍然能够执行。
调用join()方法可以理解为插队,待此线程执行完成后再执行其他线程,其他线程会进入到阻塞状态。
两者使用示例:
Thread.yield();
Thread t=xxxx;
t.join();
3.5 线程状态的观测
线程可以处在一下几个状态之一:
- NEW:尚未启动的线程
- RUNNABLE:执行中的线程
- BLOCKED:被阻塞等待监视器锁定的线程
- WAITING:等待另一线程执行特定动作的线程
- TIMED_WAITING:等待另一线程执行动作达到指定时间的线程
- TERMINATED:已退出的线程
通过调用Thread类的对象的getState()方法可以得到线程的状态,使用示例:
Thread t=xxxx;
Thread.State state=t.getState();
System.out.println(state);
4、线程的优先级与守护线程
4.1 线程的优先级
Java提供一个线程调度器来监控进入就绪状态的所有线程,高优先级线程相比于低优先级线程被分配CPU的概率更大,但不是一定先于低优先级线程执行,调度是随机无规律的因此不同线程之间不能有先后依赖关系。线程优先级是范围1-10的数字,默认优先级为5,可以通过getPriority()和setPriority()方法来获得与设置线程优先级。
4.2 守护线程
线程分为用户线程和守护线程,虚拟机必须保证用户线程执行完毕但不用保证守护线程。通过给Thread对象的setDaemon方法传递参数true(默认为false)可以设定线程为守护线程。
5、线程同步
5.1 线程不安全案例
考虑以下代码的执行:
class BuyTicket implements Runnable{
private int tickets=100;
boolean flag=true;
@Override
public void run() {
while(flag)
buy();
}
private void buy(){
if(tickets<=0){
flag=false;
return ;
}
System.out.println(Thread.currentThread().getName()+"买到第"+tickets--+"张票");
}
}
public class Myjava {
public static void main(String[] args) throws Exception{
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket,"A").start();
new Thread(buyTicket,"B").start();
new Thread(buyTicket,"C").start();
}
}
执行结果中出现了不同线程买了同一张票,这是因为未加锁,对同一对象不同线程访问到了“同一张票”
5.2 线程同步机制
线程同步其实就是一种等待机制,多个访问同一对象的线程形成一个队列待前面的处理完再执行。为保证数据在方法中能被正确访问,在访问时加入锁机制synchronized,当一个线程获得锁时将独占资源,其他线程必须等待锁释放。也存在一些问题:多线程竞争下加锁和释放锁会影响性能;优先级高的线程等待优先级低的线程释放锁也会引起性能问题。
5.3 同步方法与同步块
-
同步方法
使用synchronized关键字,同步方法不需要指定锁,默认是this或者class对象,示例:
public synchronized void method(){}
缺陷:方法里不是所有内容都需要加锁,将一个大的方法声明为synchronized方法可能影响效率
-
同步块
使用格式:synchronized(Obj){ } 。Obj可以为任何对象但应使用会被修改数据的对象
5.4 死锁
死锁:多个线程各自占有一些共享资源并互相等待其他线程释放锁,而导致两个或多个线程都在等待对方释放锁因而停止执行的情形。当某一同步块拥有两个以上对象的锁时就可能出现死锁问题。例如以下的情形:
if(xxx){
synchronized (A){//获得A的锁
Thread.sleep(1000);
synchronized (B){//修眠1s后想获得B的锁
}
}
}else{
synchronized (B){//获得B的锁
Thread.sleep(2000);
synchronized (A){//休眠2S后向=想获得A的锁
}
}
}
5.5 Lock锁
JDK5.0开始可以通过显式定义同步锁对象来实现同步;ReentrantLock类实现了Lock,它与syncronized有相同的并发性质,并可以显式的加锁与释放锁。
使用示例:
class mylock implements Runnable{
private int tickets=100;
private boolean flag=true;
private ReentrantLock Lock=new ReentrantLock(); //定义lock锁
@Override
public void run() {
while(flag)
buy();
}
private void buy(){
Lock.lock(); //显式加锁
if(tickets<=0){
flag=false;
Lock.unlock(); //显式释放锁
return ;
}
System.out.println(Thread.currentThread().getName()+"买到第"+tickets--+"张票");
Lock.unlock();
}
}
使用Lock锁后输出没有问题。
5.6 线程池
线程池思想:提前创建好多个线程放入线程池中,需要使用时直接获取,使用完后再放回池中以避免频繁的创建与销毁。可以提高响应速度降低资源消耗。
使用示例:
class Mypool implements Runnable{
@Override
public void run() {
}
}
public class Myjava {
public static void main(String[] args) throws Exception{
//创建线程池
ExecutorService service= Executors.newFixedThreadPool(3);
//使用线程池中的线程执行线程体
service.execute(new Mypool());、
//关闭链接
service.shutdown();
}
}