分類: 3C資訊

  • Flutter學習筆記(36)–常用內置動畫,Flutter學習筆記(36)–常用內置動畫

    Flutter學習筆記(36)–常用內置動畫,Flutter學習筆記(36)–常用內置動畫

    如需轉載,請註明出處:Flutter學習筆記(36)–常用內置動畫

    Flutter給我們提供了很多而且很好用的內置動畫,這些動畫僅僅需要簡單的幾行代碼就可以實現一些不錯的效果,Flutter的動畫分為補間動畫和基於物理的動畫,基於物理的動畫我們先不說。

    補間動畫很簡單,Android裏面也有補間動畫,就是給UI設置初始的狀態和結束狀態,經過我們定義的一段時間,系統去幫助我們實現開始到結束的過渡變化,這就是補間動畫。

    今天我們要看的Flutter的內置動畫就是補間動畫,根據Flutter提供的動畫組件,我們去設置初始、結尾的狀態,並且定義一下這個變化過程所需要的時間,再經過系統的處理(其實就是setState())來達到動畫的效果。

    接下來我們會寫一下常用的內置動畫組件,並且提供一下動畫效果的gif,方便大家更直觀的去理解。

    • AnimatedContainer

    看到Container我們就會知道這是一個帶有動畫屬性的容器組件,這個組件可以定義大小、顏色等屬性,那麼我們是不是就可以給這個組件設置初始和結束的大小及顏色的屬性值,然後通過系統來幫助我們來補足中間過程的動畫呢?

    答案是可以的,下面看一下demo和動畫效果:

    class _MyHomePageState extends State<MyHomePage> {
      double _width = 100.0;
      double _height = 100.0;
      Color _color = Colors.red;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: AnimatedContainer(
            width: _width,
            height: _height,
            duration: Duration(seconds: 2),
            color: _color,
            curve: Curves.bounceInOut,
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              setState(() {
                _width = 300.0;
                _height = 300.0;
                _color = Colors.green;
              });
            },
            tooltip: 'Increment',
            child: Icon(Icons.adjust),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }

     

    demo很簡單,就是先定義好組件初始的大小和顏色,點擊按鈕,在按鈕事件裏面去更改大小和顏色的屬性值。這裏唯一需要特別說一下就是curve這個屬性。

    curve指的是動畫曲線?我開始的時候不理解這個動畫曲線是什麼意思,後來看了一組圖之後,豁然開朗。demo裏面curve我們用的是Curves.bounceInOut。如下:

     

    它其實就是一個非線性的動畫的變化形式(變化過程)也可以理解為就是一種函數,也不知道這麼說大家能不能理解。

    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in_out.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_out.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_decelerate.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_sine.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quad.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_cubic.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quart.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quint.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_expo.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_circ.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_back.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_sine.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quad.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_cubic.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quart.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quint.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_expo.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_circ.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_back.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_sine.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quad.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_cubic.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quart.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quint.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_expo.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_circ.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_back.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_in.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_in_out.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_out.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_fast_out_slow_in.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_slow_middle.mp4}
    /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_linear.mp4}

     

    這裡是每一種curve曲線的表現形式,大家可以看看,也可以在demo裏面多嘗試,或者可以看另一篇博客,有動畫曲線Curves 效果。

    • AnimatedCrossFade

    Flutter中文網:一個widget,在兩個孩子之間交叉淡入,並同時調整他們的尺寸。

    個人說明:CrossFade,故名思意,淡入淡出,AnimatedCrossFade組件包含兩個子widget,一個firstChild一個secondChild,這兩個組件根據狀態(我們自己定義的一個標識)改變狀態,

    一個淡入,一個淡出,同時改變大小或顏色等屬性。

    class _MyHomePageState extends State<MyHomePage> {
      bool _showFirst = true;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: AnimatedCrossFade(
            firstChild: Container(
              width: 100,
              height: 100,
              color: Colors.red,
              alignment: Alignment.center,
              child: Text('firstChild'),
            ),
            secondChild: Container(
              width: 200,
              height: 200,
              color: Colors.green,
              alignment: Alignment.center,
              child: Text('secondChild'),
            ),
            duration: Duration(seconds: 3),
            crossFadeState:
                _showFirst ? CrossFadeState.showFirst : CrossFadeState.showSecond,
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              setState(() {
                _showFirst = false;
              });
            },
            tooltip: 'Increment',
            child: Icon(Icons.adjust),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }

     

    • Hero

    Hero常用於頁面跳轉的過長動畫,比如電商App有一個商品列表,列表的每個item都有一張縮略圖,點擊會跳轉到詳情頁面,在Flutter中將圖片從一個路由飛到另一個路由稱為hero動畫,儘管相同的動作有時也稱為 共享元素轉換

    class _MyHomePageState extends State<MyHomePage> {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Hero(
            tag: 'heroTag',
            child: ClipOval(
              child: Image.asset(
                'images/banner.png',
                width: 60,
                height: 60,
                fit: BoxFit.cover,
              ),
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              setState(() {
                Navigator.push(context, MaterialPageRoute(builder: (_) {
                  return new HeroPage();
                }));
              });
            },
            tooltip: 'Increment',
            child: Icon(Icons.adjust),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }

     

    詳情頁面:

    import 'package:flutter/material.dart';
    
    class HeroPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'HeroPage',
          home: Scaffold(
            appBar: AppBar(
              title: Text('HeroPage'),
            ),
            body: Center(
              child: GestureDetector(
                child: Hero(
                  tag: 'heroTag',
                  child: ClipOval(
                    child: Image.asset(
                      'images/banner.png',
                      width: 300,
                      height: 300,
                      fit: BoxFit.cover,
                    ),
                  ),
                ),
                onTap: () {
                  Navigator.pop(context);
                },
              ),
            ),
          ),
        );
      }
    }

     

    注:特彆強調一下,為了將兩個頁面的元素關聯起來,hero有個tag標識,前後兩個頁面的tag標識必須一樣,不然的話元素是關聯不起來的,也就意味着不會產生hero動畫。

    1.同級tag不允許相同。

    2.前後頁面想要有hero動畫,tag必須相同。

    3.前後關聯起來的hero組件,其各自內部的child組件不是必須一樣的,就是說前面的hero的子組件可以是image,後面的hero的子組件可以是image以外的其他組件。

    • AnimatedBuilder

    class _MyHomePageState extends State<MyHomePage>
        with SingleTickerProviderStateMixin {
      Animation<double> _animation;
      AnimationController _animationController;
    
      @override
      void initState() {
        _animationController =
            AnimationController(duration: Duration(seconds: 3), vsync: this);
        _animation =
            new Tween(begin: 0.0, end: 200.0).animate(_animationController);
        _animationController.forward();
        super.initState();
      }
    
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: Text(widget.title),
            ),
            body: AnimatedBuilder(
              animation: _animation,
              builder: (BuildContext context, Widget child) {
                return Center(
                  child: Container(
                    color: Colors.red,
                    width: _animation.value,
                    height: _animation.value,
                    child: child,
                  ),
                );
              },
            ));
      }
    }

    AnimationController:動畫控制器(定義動畫過程時長)。

    Animation:動畫變化區間值(也可以說是開始和結束的關鍵幀值),demo里定義的值為初始0,結束200。

    _animation.value:關鍵幀值是0和200,_animation.value的值為0–200之間連續變化的值(0-1-2-3-…-198-199-200)。

    • DecoratedBoxTransition

    Decortated可以給容器添加各種外觀裝飾,比如增加圓角、陰影等裝飾。DecoratedBox的動畫版本,可以給它的Decoration不同屬性使用動畫

    class _MyHomePageState extends State<MyHomePage>
        with SingleTickerProviderStateMixin {
      Animation<Decoration> _animation;
      AnimationController _animationController;
    
      @override
      void initState() {
        _animationController =
            AnimationController(duration: Duration(seconds: 3), vsync: this);
        _animation = DecorationTween(
                begin: BoxDecoration(
                    borderRadius: BorderRadius.all(Radius.circular(0.0)),
                    color: Colors.red),
                end: BoxDecoration(
                    borderRadius: BorderRadius.all(Radius.circular(30.0)),
                    color: Colors.green))
            .animate(_animationController);
        _animationController.forward();
        super.initState();
      }
    
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text(widget.title)),
          body: Center(
            child: DecoratedBoxTransition(
              decoration: _animation,
              child: Container(
                width: 100,
                height: 100,
              ),
            ),
          ),
        );
      }
    }

    • FadeTransition

    透明度變化動畫,因為透明度也是在0-1之間變化的,所以animation就還繼續用double類型的就可以了。

    class _MyHomePageState extends State<MyHomePage>
        with SingleTickerProviderStateMixin {
      Animation<double> _animation;
      AnimationController _animationController;
    
      @override
      void initState() {
        _animationController =
            AnimationController(duration: Duration(seconds: 2), vsync: this);
        _animation = Tween(begin: 1.0, end: 0.0).animate(_animationController);
        _animationController.forward();
        super.initState();
      }
    
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text(widget.title)),
          body: Center(
            child: FadeTransition(
              opacity: _animation,
              child: Container(
                width: 100,
                height: 100,
                decoration: BoxDecoration(
                  color: Colors.red,
                ),
              ),
            ),
          ),
        );
      }
    }

    • RotationTransition

    旋轉動畫,對widget使用旋轉動畫 1~360°(Tween(begin: 0.0, end: 1.0))這裏的0-1指的是0°-360°

    class _MyHomePageState extends State<MyHomePage>
        with SingleTickerProviderStateMixin {
      Animation<double> _animation;
      AnimationController _animationController;
    
      @override
      void initState() {
        _animationController =
            AnimationController(duration: Duration(seconds: 2), vsync: this);
        _animation = Tween(begin: 0.0, end: 1.0).animate(_animationController);
        _animationController.forward();
        super.initState();
      }
    
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text(widget.title)),
          body: Center(
            child: RotationTransition(
              turns: _animation,
              child: Container(
                width: 100,
                height: 100,
                decoration: BoxDecoration(
                  color: Colors.red,
                ),
                child: Center(child: Text('data')),
              ),
            ),
          ),
        );
      }
    }

    • ScaleTransition

    縮放動畫,Tween(begin: 1.0, end: 0.2)指的是原大小的倍數,demo里是由原大小縮小到原來的0.2倍。

    class _MyHomePageState extends State<MyHomePage>
        with SingleTickerProviderStateMixin {
      Animation<double> _animation;
      AnimationController _animationController;
    
      @override
      void initState() {
        _animationController =
            AnimationController(duration: Duration(seconds: 2), vsync: this);
        _animation = Tween(begin: 1.0, end: 0.2).animate(_animationController);
        _animationController.forward();
        super.initState();
      }
    
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text(widget.title)),
          body: Center(
            child: ScaleTransition(
              scale: _animation,
              child: Container(
                width: 200,
                height: 200,
                decoration: BoxDecoration(
                  color: Colors.red,
                ),
                child: Center(child: Text('data')),
              ),
            ),
          ),
        );
      }
    }

    • SizeTransition

    僅一個方向進行縮放

    class _MyHomePageState extends State<MyHomePage>
        with SingleTickerProviderStateMixin {
      Animation<double> _animation;
      AnimationController _animationController;
    
      @override
      void initState() {
        _animationController =
            AnimationController(duration: Duration(seconds: 2), vsync: this);
        _animation = Tween(begin: 1.0, end: 0.2).animate(_animationController);
        _animationController.forward();
        super.initState();
      }
    
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text(widget.title)),
          body: Center(
            child: SizeTransition(
              axis: Axis.horizontal,
              sizeFactor: _animation,
              child: Center(
                child: Container(
                  width: 200,
                  height: 200,
                  decoration: BoxDecoration(
                    color: Colors.red,
                  ),
                  child: Center(child: Text('data')),
                ),
              ),
            ),
          ),
        );
      }
    }

     

    以上!有任何疑問歡迎留言!

     

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※教你寫出一流的銷售文案?

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※回頭車貨運收費標準

    ※別再煩惱如何寫文案,掌握八大原則!

    ※超省錢租車方案

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • Spring系列.事務管理原理簡析

    Spring的事務管理功能能讓我們非常簡單地進行事務管理。只需要進行簡單的兩步配置即可:

    step1:開啟事務管理功能

    @Configuration
    //@EnableTransactionManagement註解有以下幾個屬性
    //proxyTargetClass屬相:指定事務的AOP是通過JDK動態代理實現,還是CGLIB動態代理實現。true的話是CGLIB,false的話是JDK動態代理
    //                     需要注意的是這個屬性只有在AdviceMode設置成AdviceMode.PROXY的情況下才會生效,加入使用ASPECTJ這AOP框架的話,這個屬性就失效了。
    //                     另外,這個屬性的設定可能會影響其他需要動態代理的類。比如說將這個屬性設置成true,@Async註解的方法也會使用CGLIB生成代理類。
    //                     但是總的來說,這個屬性的設置不會造成什麼負面影響,畢竟JDK動態代理和CGLIB動態代理都能實現我們的需求
    
    //mode屬性:Spring提供的AOP功能有兩種實現方式,一種是Spring自帶的AOP功能,主要靠JDK代理和CGLIB代理實現,另外一種是通過第三方框架ASPECTJ實現。這個選項
    //        就是設定Spring用哪種方式提供AOP功能。AdviceMode.PROXY表示用Spring自帶的AOP功能,AdviceMode.ASPECTJ表示使用AdviceMode提供AOP功能。
    //        需要注意的是Spring自帶的AOP功能不支持本地調用的代理功能,也就是說同一個類中的方法互相調用不會“觸發”代理方法。如果想讓自調用觸發代理,可以考慮使用ASPECTJ。
    
    //order屬性:表示當一個連接點(方法)被切多次時(也就是說有多個Advice和連接點關聯),這些連接點的執行順序。
    @EnableTransactionManagement
    public class TxConfig {
    }
    

    step2:在需要事務管理的方法上添加@Transactional註解

    @Override
    @Transactional
    public int saveSysUser(SysUser user) {
        int i = sysUserMapper.insert(user);
        return i;
    }
    

    整個使用流程就這麼簡單。這篇博客就來簡單分析下Spring是怎麼實現事務管理的。

    對事務管理進行AOP的過程

    Spring的很多功能都是通過AOP功能實現的,事務管理也是。我們之前的文章分析過Spring基礎AOP實現的原理。這邊再簡單提下Spring實現AOP的
    原理:

    Spring基礎的AOP功能的開關是@EnableAspectJAutoProxy,這個註解註冊了一個Bean——AnnotationAwareAspectJAutoProxyCreator,這個Bean才是實現AOP功能的關鍵。
    這個Bean實現了InstantiationAwareBeanPostProcessor接口(這個接口是BeanPostProcessor的子接口)。熟悉Spring的讀者知道,實現BeanPostProcessor接口的Bean
    會在其他Bean初始化之前初始,然後在其他Bean初始化的時候,BeanPostProcessor的實現會對這些Bean進行“加工處理”。

    這邊AnnotationAwareAspectJAutoProxyCreator就承擔了加工處理類的角色。這個Bean在其他Bean初始化前後會判斷這個Bean中的方法是不是有對應的Advice,如果有的話就會
    通過動態代理的方式生成動態代理類將通知織入進去。

    我們發現開啟事務管理的方式和開啟AOP功能的方式很像,也是通過Enable註解開啟。所以很自然就猜想事務管理是不是也是通過BeanPostProcessor的方式實現的。帶着這個猜想去看下@EnableTransactionManagement註解。

    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Import(TransactionManagementConfigurationSelector.class)
    public @interface EnableTransactionManagement {
    
    	boolean proxyTargetClass() default false;
    	AdviceMode mode() default AdviceMode.PROXY;
    	int order() default Ordered.LOWEST_PRECEDENCE;
    }
    
    

    看到上面的代碼,我們很自然的會去看TransactionManagementConfigurationSelector的代碼。Spring有兩種方式提供AOP功能,一種是自帶的動態代理的功能,一種是
    通過ASPECTJ的方式提供。這邊主要討論Spring自帶的AOP功能。

    protected String[] selectImports(AdviceMode adviceMode) {
        switch (adviceMode) {
            //用代理的方式實現事務管理的AOP功能
            case PROXY:
                return new String[] {AutoProxyRegistrar.class.getName(),
                        ProxyTransactionManagementConfiguration.class.getName()};
            case ASPECTJ:
                return new String[] {determineTransactionAspectClass()};
            default:
                return null;
        }
    }
    

    上面的代碼中,我們主要關注PROXY這個case中的方法。這個case中註冊了兩個類:AutoProxyRegistrar和ProxyTransactionManagementConfiguration。

    首先我們來看AutoProxyRegistrar這個類,層層點進入,我們發現這個類最終就是註冊了InfrastructureAdvisorAutoProxyCreator這個類。仔細看InfrastructureAdvisorAutoProxyCreator
    這個類實現的接口的話,你會發現這個類也是BeanPostProcesser系列的類。看到這裏,我的直覺是事務管理的AOP過程和Spring基礎的AOP功能原理可能是一樣的。

    再仔細看InfrastructureAdvisorAutoProxyCreator對BeanPostProcesser系列接口的實現,你會發現都是繼承的AbstractAutoProxyCreator。看到這個驗證了我之前的想法。

    下面是Spring對事務管理進行AOP的過程,你會發現和基礎的AOP功能是一套代碼。

    @Override
    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
        if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
    }
    
    
    protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
        if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
            return bean;
        }
        if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
        }
        if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }
    
        // Create proxy if we have advice.
        // 代碼1
        // 這邊是獲取Advice和Advisor的具體代碼
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
        if (specificInterceptors != DO_NOT_PROXY) {
            this.advisedBeans.put(cacheKey, Boolean.TRUE);
            //生成代理類
            Object proxy = createProxy(
                    bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
            this.proxyTypes.put(cacheKey, proxy.getClass());
            return proxy;
        }
    
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }
    
    

    再來看看ProxyTransactionManagementConfiguration做了些啥?點進源代碼你會發現這個類的功能很簡單,就是註冊了下面幾個事務管理相關的基礎Bean。

    • BeanFactoryTransactionAttributeSourceAdvisor;
    • TransactionAttributeSource;
    • TransactionInterceptor。

    事務管理的生效過程

    上面的章節中講了Spring是怎麼生成事務相關的AOP代理類的。這邊來講下Spring的事務管理是怎麼生效的——怎麼開啟事務,怎麼回滾事務,怎麼提交事務,Spring中的事務傳播
    機制是怎麼生效的。

    這塊的代碼主要是在TransactionAspectSupport的invokeWithinTransaction方法中(不要問我是怎麼找到這段代碼的…)。下面講下這個方法中的幾個關鍵點。

    
    protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
    			final InvocationCallback invocation) throws Throwable {
    
        // If the transaction attribute is null, the method is non-transactional.
        TransactionAttributeSource tas = getTransactionAttributeSource();
        //獲取TransactionAttribute,這個類主要是@Transactional註解的配置信息
        final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
        //確認事務管理器
        final TransactionManager tm = determineTransactionManager(txAttr);
    
        if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
            ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
                if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
                    throw new TransactionUsageException(
                            "Unsupported annotated transaction on suspending function detected: " + method +
                            ". Use TransactionalOperator.transactional extensions instead.");
                }
                ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
                if (adapter == null) {
                    throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
                            method.getReturnType());
                }
                return new ReactiveTransactionSupport(adapter);
            });
            return txSupport.invokeWithinTransaction(
                    method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
        }
    
        PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
        final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
    
        if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
            // Standard transaction demarcation with getTransaction and commit/rollback calls.
            TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
    
            Object retVal;
            try {
                // This is an around advice: Invoke the next interceptor in the chain.
                // This will normally result in a target object being invoked.
                retVal = invocation.proceedWithInvocation();
            }
            catch (Throwable ex) {
                // target invocation exception
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            }
            finally {
                cleanupTransactionInfo(txInfo);
            }
    
            if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
                // Set rollback-only in case of Vavr failure matching our rollback rules...
                TransactionStatus status = txInfo.getTransactionStatus();
                if (status != null && txAttr != null) {
                    retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                }
            }
    
            commitTransactionAfterReturning(txInfo);
            return retVal;
        }else {
            final ThrowableHolder throwableHolder = new ThrowableHolder();
    
            // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
            try {
                Object result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
                    TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
                    try {
                        Object retVal = invocation.proceedWithInvocation();
                        if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
                            // Set rollback-only in case of Vavr failure matching our rollback rules...
                            retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                        }
                        return retVal;
                    }
                    catch (Throwable ex) {
                        if (txAttr.rollbackOn(ex)) {
                            // A RuntimeException: will lead to a rollback.
                            if (ex instanceof RuntimeException) {
                                throw (RuntimeException) ex;
                            }
                            else {
                                throw new ThrowableHolderException(ex);
                            }
                        }
                        else {
                            // A normal return value: will lead to a commit.
                            throwableHolder.throwable = ex;
                            return null;
                        }
                    }
                    finally {
                        cleanupTransactionInfo(txInfo);
                    }
                });
    
                // Check result state: It might indicate a Throwable to rethrow.
                if (throwableHolder.throwable != null) {
                    throw throwableHolder.throwable;
                }
                return result;
            }
            catch (ThrowableHolderException ex) {
                throw ex.getCause();
            }
            catch (TransactionSystemException ex2) {
                if (throwableHolder.throwable != null) {
                    logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                    ex2.initApplicationException(throwableHolder.throwable);
                }
                throw ex2;
            }
            catch (Throwable ex2) {
                if (throwableHolder.throwable != null) {
                    logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                }
                throw ex2;
            }
        }
    }
    
    

    事務操作的主要代碼都在這個方法中,要詳細將這個方法能寫很多內容。這邊就不詳細展開了,大家感興趣的可以仔細研究下這個方法。

    重要類總結

    • InfrastructureAdvisorAutoProxyCreator:事務管理AOP註冊
    • BeanFactoryTransactionAttributeSourceAdvisor:Spring事務管理基礎Bean
    • TransactionAttributeSource:Spring事務管理基礎Bean
    • TransactionInterceptor:Spring事務管理基礎Bean
    • TransactionAspectSupport的invokeWithinTransaction方法:事務處理的主要方法

    相關註解

    如果你仔細看過Spring的相關源代碼,會發現Spring的Enable系列的註解都是上面的“套路”,熟悉了@EnableTransactionManagement註解生效的原理,其他註解都是類似的生效規則。比如

    • @EnableAsync
    • @EnableScheduling

    希望大家能做到觸類旁通。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※超省錢租車方案

    ※別再煩惱如何寫文案,掌握八大原則!

    ※回頭車貨運收費標準

    ※教你寫出一流的銷售文案?

    FB行銷專家,教你從零開始的技巧

  • Java多線程之volatile詳解

    Java多線程之volatile詳解

    本文目錄

    • 從多線程交替打印A和B開始
    • Java 內存模型中的可見性、原子性和有序性
    • Volatile原理
      • volatile的特性
      • volatile happens-before規則
      • volatile 內存語義
      • volatile 內存語義的實現
    • CPU對於Volatile的支持
      • 緩存一致性協議
    • 工作內存(本地內存)並不存在
    • 總結
    • 參考資料

    從多線程交替打印A和B開始

    面試中經常會有一道多線程交替打印A和B的問題,可以通過使用Lock和一個共享變量來完成這一操作,代碼如下,其中使用num來決定當前線程是否打印

    public class ABTread {
    
        private static int num=0;
        private static Lock lock=new ReentrantLock();
    
        public static void main(String[] args) throws InterruptedException {
    
            Thread A=new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true){
                        lock.lock();
                        if (num==0){
                            System.out.println("A");
                            num=1;
                        }
                        lock.unlock();
                    }
                }
            },"A");
            Thread B=new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true){
                        lock.lock();
                        if (num==1){
                            System.out.println("B");
                            num=0;
                        }
                        lock.unlock();
                    }
                }
            },"B");
            A.start();
            B.start();
        }
    }
    

    這一過程使用了一個可重入鎖,在以前可重入鎖的獲取流程中有分析到,當鎖被一個線程持有時,後繼的線程想要再獲取鎖就需要進入同步隊列還有可能會被阻塞。
    現在假設當A線程獲取了鎖,B線程再來獲取鎖且B線程獲取失敗則會調用LockSupport.park()導致線程B阻塞,線程A釋放鎖時再還行線程B
    是否會經常存在阻塞線程和還行線程的操作呢,阻塞和喚醒的操作是比較費時間的。是否存在一個線程剛釋放鎖之後這一個線程又再一次獲取鎖,由於共享變量的存在,
    則獲取鎖的線程一直在做着毫無意義的事情。

    可以使用volatile關鍵字來修飾共享變量來解決,代碼如下:

    public class ABTread {
    
        private static volatile  int num=0;
        public static void main(String[] args) throws InterruptedException {
    
            Thread A=new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true){
                        if (num==0){        //讀取num過程記作1
                            System.out.println("A");
                            num=1;          //寫入num記位2
                        }
                    }
                }
            },"A");
            Thread B=new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true){
                        if (num==1){        //讀取num過程記作3
                            System.out.println("B");
                            num=0;          ////寫入num記位4
                        }
                    }
                }
            },"B");
            A.start();
            B.start();
        }
    }
    

    Lock可以通過阻止同時訪問來完成對共享變量的同時訪問和修改,必要的時候阻塞其他嘗試獲取鎖的線程,那麼volatile關鍵字又是如何工作,
    在這個例子中,是否效果會優於Lock呢。

    Java 內存模型中的可見性、原子性和有序性

    • 可見性:指線程之間的可見性,一個線程對於狀態的修改對另一個線程是可見的,也就是說一個線程修改的結果對於其他線程是實時可見的。
      可見性是一個複雜的屬性,因為可見性中的錯誤總是會違背我們的直覺(JMM決定),通常情況下,我們無法保證執行讀操作的線程能實時的看到其他線程的寫入的值。
      為了保證線程的可見性必須使用同步機制。退一步說,最少應該保證當一個線程修改某個狀態時,而這個修改時程序員希望能被其他線程實時可見的,
      那麼應該保證這個狀態實時可見,而不需要保證所有狀態的可見。在 Javavolatilesynchronizedfinal 實現可見性。

    • 原子性:如果一個操作是不可以再被分割的,那麼我們說這個操作是一個原子操作,即具有原子性。但是例如i++實際上是i=i+1這個操作是可分割的,他不是一個原子操作。
      非原子操作在多線程的情況下會存在線程安全性問題,需要是我們使用同步技術將其變為一個原子操作。javaconcurrent包下提供了一些原子類,
      我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicIntegerAtomicLongAtomicReference等。在 Javasynchronized 和在 lockunlock 中操作保證原子性

    • 有序性:一系列操作是按照規定的順序發生的。如果在本線程之內觀察,所有的操作都是有序的,如果在其他線程觀察,所有的操作都是無序的;前半句指“線程內表現為串行語義”後半句指“指令重排序”和“工作內存和主存同步延遲”
      Java 語言提供了 volatilesynchronized 兩個關鍵字來保證線程之間操作的有序性。volatile 是因為其本身包含“禁止指令重排序”的語義,
      synchronized 是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規則獲得的,此規則決定了持有同一個對象鎖的兩個同步塊只能串行執行。

    Volatile原理

    volatile定義:Java編程語言允許線程訪問共享變量,為了確保共享變量能被準確和一致的更新,線程應該通過獲取排他鎖單獨獲取這個變量;
    java提供了volatile關鍵字在某些情況下比鎖更好用。

    • Java語言提供了volatile了關鍵字來提供一種稍弱的同步機制,他能保證操作的可見性和有序性。當把變量聲明為volatile類型后,
      編譯器與運行時都會注意到這個變量是一個共享變量,並且這個變量的操作禁止與其他的變量的操作重排序。

    • 訪問volatile變量時不會執行加鎖操作。因此也不會存在阻塞競爭的線程,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。

    volatile的特性

    volatile具有以下特性:

    • 可見性:對於一個volatile的讀總能看到最後一次對於這個volatile變量的寫
    • 原子性:對任意單個volatile變量的讀/寫具有原子性,但對於類似於i++這種複合操作不具有原子性。
    • 有序性:

    volatile happens-before規則

    根據JMM要求,共享變量存儲在共享內存當中,工作內存存儲一個共享變量的副本,
    線程對於共享變量的修改其實是對於工作內存中變量的修改,如下圖所示:

    從多線程交替打印A和B開始章節中使用volatile關鍵字的實現為例來研究volatile關鍵字實現了什麼:
    假設線程A在執行num=1之後B線程讀取num指,則存在以下happens-before關係

    1)  1 happens-before 2,3 happens-before 4
    2)  根據volatile規則有:2 happens-before 3
    3)  根據heppens-before傳遞規則有: 1 happens-before 4
    

    至此線程的執行順序是符合我們的期望的,那麼volatile是如何保證一個線程對於共享變量的修改對於其他線程可見的呢?

    volatile 內存語義

    根據JMM要求,對於一個變量的獨寫存在8個原子操作。對於一個共享變量的獨寫過程如下圖所示:

    對於一個沒有進行同步的共享變量,對其的使用過程分為readloaduseassign以及不確定的storewrite過程。
    整個過程的語言描述如下:

    - 第一步:從共享內存中讀取變量放入工作內存中(`read`、`load`)
    - 第二步:當執行引擎需要使用這個共享變量時從本地內存中加載至**CPU**中(`use`)
    - 第三步:值被更改后使用(`assign`)寫回工作內存。
    - 第四步:若之後執行引擎還需要這個值,那麼就會直接從工作內存中讀取這個值,不會再去共享內存讀取,除非工作內存中的值出於某些原因丟失。
    - 第五步:在不確定的某個時間使用`store`、`write`將工作內存中的值回寫至共享內存。
    

    由於沒有使用鎖操作,兩個線程可能同時讀取或者向共享內存中寫入同一個變量。或者在一個線程使用這個變量的過程中另一個線程讀取或者寫入變量。
    上圖中1和6兩個操作可能會同時執行,或者在線程1使用num過程中6過程執行,那麼就會有很嚴重的線程安全問題,
    一個線程可能會讀取到一個並不是我們期望的值。

    那麼如果希望一個線程的修改對後續線程的讀立刻可見,那麼只需要將修改后存儲在本地內存中的值回寫到共享內存
    並且在另一個線程讀的時候從共享內存重新讀取而不是從本地內存中直接讀取即可;事實上
    當寫一個volatile變量時,JMM會把該線程對應的本地內存中共享變量值刷新會共享內存;
    而當讀取一個volatile變量時,JMM會從主存中讀取共享變量
    ,這也就是volatile的寫-讀內存語義。

    volatile的寫-讀內存語義:

    • volatile寫的內存語義:當寫一個volatile變量時,JMM會把該線程對應的本地內存中共享變量值刷新會共享內存
    • volatile讀的內存語義:當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效,線程接下來將從主內存中讀取共享變量。

    如果將這兩個步驟綜合起來,那麼線程3讀取一個volatile變量后,寫線程1在寫這個volatile變量之前所有可見的共享變量的值都將樂客變得對線程3可見。

    volatile變量的讀寫過程如下圖:

    需要注意的是:在各個線程的工作內存中是存在volatile變量的值不一致的情況的,只是每次使用都會從共享內存讀取並刷新,執行引擎看不到不一致的情況,
    所以認為volatile變量在本地內存中不存在不一致問題。

    volatile 內存語義的實現

    在前文Java內存模型中有提到重排序。為了實現volatile的內存語義,JMM會限制重排序的行為,具體限制如下錶:

    是否可以重排序 第二個操作 第二個操作 第二個操作
    第一個操作 普通讀/寫 volatile volatile
    普通讀/寫 NO
    volatile NO NO NO
    volatile NO NO

    說明:

    - 若第一個操作時普通變量的讀寫,第二個操作時volatile變量的寫操作,則編譯器不能重排序這兩個操作
    - 若第一個操作是volatile變量的讀操作,不論第二個變量是什麼操作不餓能重排序這兩個操作
    - 若第一個操作時volatile變量的寫操作,除非第二個操作是普通變量的獨寫,否則不能重排序這兩個操作
    

    為了實現volatile變量的內存語義,編譯器生成字節碼文件時會在指令序列中插入內存屏障來禁止特定類型的處理器排序。
    為了實現volatile變量的內存語義,插入了以下內存屏障,並且在實際執行過程中,只要不改變volatile的內存語義,
    編譯器可以根據實際情況省略部分不必要的內存屏障

    - 在每個volatile寫操作前面插入StoreStore屏障
    - 在每個volatile寫操作後面插入StoreLoad屏障
    - 在每個volatile讀操作後面插入LoadLoad屏障
    - 在每個volatile讀操作後面插入LoadStore屏障
    

    插入內存屏障后volatile寫操作過程如下圖:

    插入內存屏障后volatile讀操作過程如下圖:

    至此在共享內存和工作內存中的volatile的寫-讀的工作過程全部完成

    但是現在的CPU中存在一個緩存,CPU讀取或者修改數據的時候是從緩存中獲取並修改數據,那麼如何保證CPU緩存中的數據與共享內存中的一致,並且修改后寫回共享內存呢?

    CPU對於Volatile的支持

    緩存行:cpu緩存存儲數據的基本單位,cpu不能使數據失效,但是可以使緩存行失效。

    對於CPU來說,CPU直接操作的內存時高速緩存,而每一個CPU都有自己L1、L2以及共享的L3級緩存,如下圖:

    那麼當CPU修改自身緩存中的被volatile修飾的共享變量時,如何保證對其他CPU的可見性。

    緩存一致性協議

    在多處理器的情況下,每個處理器總是嗅探總線上傳播的數據來檢查自己的緩存是否過期,當處理器發現自己對應的緩存對應的地址被修改,
    就會將當前處理器的緩存行設置為無效狀態,當處理器對這個數據進行操作的時候,會重新從系統中把數據督導處理器的緩存里。這個協議被稱之為緩存一致性協議。

    緩存一致性協議的實現又MEIMESIMOSI等等。

    MESI協議緩存狀態

    狀態 描述
    M(modified)修改 該緩存指被緩存在該CPU的緩存中並且是被修改過的,即與主存中的數據不一致,該緩存行中的數據需要在未來的某個時間點寫回主存,當寫回註冊年之後,該緩存行的狀態會變成E(獨享)
    E(exclusive)獨享 該緩存行只被緩存在該CPU的緩存中,他是未被修改過的,與主存中數據一致,該狀態可以在任何時候,當其他的CPU讀取該內存時編程共享狀態,同樣的,當CPU修改該緩存行中的內容時,該狀態可以變為M(修改)
    S(share)共享 該狀態意味着該緩存行可能被多個CPU緩存,並且各個緩存中的數據與主存中的數據一致,當有一個CPU修改自身對應的緩存的數據,其它CPU中該數據對應的緩存行被作廢
    I(Invalid)無效 該緩存行無效

    MESI協議可以防止緩存不一致的情況,但是當一個CPU修改了緩存中的數據,但是沒有寫入主存,也會存在問題,那麼如何保證CPU修改共享被volatile修飾的共享變量后立刻寫回主存呢。

    在有volatile修飾的共享變量進行寫操作的時候會多出一條帶有lock前綴的彙編代碼,而這個lock操作會做兩件事:

    1. 將當前處理器的緩存行的數據協會到系統內存。lock信號確保聲言該信號期間CPU可以獨佔共享內存。在之前通過鎖總線的方式,現在採用鎖緩存的方式。
    2. 這個寫回操作會使其他處理器的緩存中緩存了該地址的緩存行無效。在下一次這些CPU需要使用這些地址的值時,強制要求去共享內存中讀取。

    如果對聲明了volatile的共享變量進行寫,JVM會向CPU發送一條lock指令,使得將這個變量所在的緩存行緩存的數據寫回到內存中。而其他CPU通過嗅探總線上傳播的數據,
    使得自身緩存行失效,下一次使用時會從主存中獲取對應的變量。

    工作內存(本地內存)並不存在

    根據JAVA內存模型描述,各個線程使用自身的工作內存來保存共享變量,那麼是不是每個CPU緩存的數據就是從工作內存中獲取的。這樣的話,在CPU緩存寫回主存時,
    協會的是自己的工作內存地址,而各個線程的工作內存地址並不一樣。CPU嗅探總線時就嗅探不到自身的緩存中緩存有對應的共享變量,從而導致錯誤?

    事實上,工作內存並不真實存在,只是JMM為了便於理解抽象出來的概念,它涵蓋了緩存,寫緩衝區、寄存器及其他的硬件編譯器優化。所以緩存是直接和共享內存交互的。
    每個CPU緩存的共享數據的地址是一致的。

    總結

    • volatile提供了一種輕量級同步機制來完成同步,它可以保操作的可見性、有序性以及對於單個volatile變量的讀/寫具有原子性,對於符合操作等非原子操作不具有原子性。

    • volatile通過添加內存屏障及緩存一致性協議來完成對可見性的保證。

    最後Lock#lock()是如何保證可見性的呢??

    Lock#lock()使用了AQSstate來標識鎖狀態,而statevolatile標記的,由於對於volatile的獨寫操作時添加了內存屏障的,所以在修改鎖狀態之前,
    一定會將之前的修改寫回共享內存。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?

  • mysql定時備份任務

    mysql定時備份任務

    簡介

    在生產環境上,為了避免數據的丟失,通常情況下都會定時的對數據庫進行備份。而Linux的crontab指令則可以幫助我們實現對數據庫定時進行備份。首先我們來簡單了解crontab指令,如果你會了請跳到下一個內容mysql備份
    本文章的mysql數據庫是安裝在docker容器當中,以此為例進行講解。沒有安裝到docker容器當中也可以參照參照。

    contab定時任務

    使用crontab -e來編寫我們的定時任務。

    0 5 * * 1 [command]
    

    前面的5個数字分別代表分、時、日、月、周,後面的 command為你的執行命令。
    假如你需要在每天晚上8點整執行定時任務,那麼可以這麼寫

    0 8 * * * [command]
    

    擴展:
    crontab -l 可以查看自己的定時任務
    crontab -r 刪除當前用戶的所有定時任務

    mysql備份

    快速上手

    這裏我的mysql數據庫是docker容器。假如你需要在每天晚上8點整執行定時任務,那麼可以這麼寫。
    首先執行命令crontab -e

    0 8 * * * docker exec mysql_container mysqldump -uroot -proot_password database_name > /var/backups/mysql/$(date +%Y%m%d_%H%M%S).sql
    

    mysql_container 為你的數據庫容器名
    mysqldump 是mysql數據庫導出數據的指令
    -u 填寫root賬號
    -p 填寫root密碼
    database_name 需要備份的數據庫名
    /var/backups/mysql/$(date +%Y%m%d_%H%M%S).sql 備份文件,後面是文件名的格式

    如果你沒什麼要求,單純的只是想要備份,那麼上面那個命令就可以幫你進行定時備份。

    小坑: mysql備份的時候我使用了docker exec -it mysqldump ... 這樣的命令去做bash腳本,因為-i參數是有互動的意思,導致在crontab中執行定時任務的時候,沒有輸出數據到sql文件當中。所以使用crontab定時的對docker容器進行備份命令的時候不要添加-i參數。

    crontab優化

    我不建議直接在crontab -e裏面寫要執行的命令,任務多了就把這個文件寫的亂七八招了。
    建議把數據庫備份的命令寫成一個bash腳本。在crontab這裏調用就好了
    如:建立一個/var/backups/mysql/mysqldump.sh文件,內容如下

    docker exec mysql_container mysqldump -uroot -pmypassword database_name > /var/backups/mysql/$(date +%Y%m%d_%H%M%S).sql
    

    然後把文件改為當前用戶可執行的:

    chmod 711 /var/backups/mysql/mysqldump.sh
    

    執行crontab -e 命令修改成如下:

    0 20 * * * /var/backups/mysql/mysqldump.sh
    

    那麼這樣就比較規範了。

    mysql備份優化

    因為sql文件比較大,所以一般情況下都會對sql文件進行壓縮,不然的話磁盤佔用就太大了。
    假設你做了上面這一步 crontab優化,我們可以把mysqldump.sh腳本改成下面這樣:

    export mysqldump_date=$(date +%Y%m%d_%H%M%S) && \
    docker exec mysql_container mysqldump -uroot -pmypassword database_name> /var/backups/mysql/$mysqldump_date.sql && \
    gzip /var/backups/mysql/$mysqldump_date.sql
    find /var/backups/mysql/ -name "*.sql" -mtime +15 -exec rm -f {} \;
    

    export 在系統中自定義了個變量mysqldump_date,給備份和壓縮命令使用
    gzip 為壓縮命令,默認壓縮了之後會把源文件刪除,壓縮成.gz文件
    find ... 這行命令的意思為,查詢 /var/backups/mysql/目錄下,創建時間15天之前(-mtime +15),文件名後綴為.sql的所有文件 執行刪除命令-exec rm -f {} \;。總的意思就是:mysql的備份文件只保留15天之內的。15天之前的都刪除掉。

    數據恢復

    若一不小心你執行drop database,穩住,淡定。我們首先要創建數據庫被刪除的數據庫。

    >mysql create database database_name;
    

    然後恢復最近備份的數據。恢復備份的命令:

    docker exec -i mysql_container mysql -uroot -proot_password database_name < /var/backups/mysql/20200619_120012.sql
    

    雖然恢復了備份文件的數據,但是備份時間點之後的數據我們卻沒有恢復回來。
    如:晚上8點進行定時備份,但是卻在晚上9點drop database,那麼晚上8點到晚上9點這一個小時之內的數據卻沒有備份到。這時候就要使用binlog日誌了。

    binlog日誌

    binlog 是mysql的一個歸檔日誌,記錄的數據修改的邏輯,如:給 ID = 3 的這一行的 money 字段 + 1。
    首先登錄mysql后查詢當前有多少個binlog文件:

    > mysql show binary logs;
    +---------------+-----------+-----------+
    | Log_name      | File_size | Encrypted |
    +---------------+-----------+-----------+
    | binlog.000001 |       729 | No        |
    | binlog.000002 |      1749 | No        |
    | binlog.000003 |      1087 | No        |
    +---------------+-----------+-----------+
    

    查看當前正在寫入的binlog

    mysql> show master status\G;
    

    生成新的binlog文件,mysql的後續操作都會寫入到新的binlog文件當中,一般在恢複數據都時候都會先執行這個命令。

    mysql> flush logs
    

    查看binlog日誌

    mysql> show binlog events in 'binlog.000003';
    

    小知識點:初始化mysql容器時,添加參數--binlog-rows-query-log-events=ON。或者到容器當中修改/etc/mysql/my.cnf文件,添加參數binlog_rows_query_log_events=ON,然後重啟mysql容器。這樣可以把原始的SQL添加到binlog文件當中。

    恢複數據

    拿回上面例子的這段話。

    晚上8點進行定時備份,但是卻在晚上9點drop database,那麼晚上8點到晚上9點這一個小時之內的數據卻沒有備份到。。

    首先進入到mysql容器后,切換到/var/lib/mysql目錄下,查看binlog文件的創建日期

    cd /var/lib/mysql
    ls -l
    ...
    -rw-r----- 1 mysql mysql      729 Jun 19 15:54  binlog.000001
    -rw-r----- 1 mysql mysql     1749 Jun 19 18:45  binlog.000002
    -rw-r----- 1 mysql mysql     1087 Jun 19 20:58  binlog.000003
    ...
    

    從文件日期可以看出:當天時間為2020-06-21,binlog.000002文件的最後更新時間是 18:45 分,那麼晚上8點的備份肯定包含了binlog.000002的數據;
    binlog.000003的最後更新日期為 20:58 分,那麼我們需要恢復的數據 = 晚上8點的全量備份 + binlog.000003的 20:00 – 執行drop database命令時間前的數據。

    恢復命令格式:

    mysqlbinlog [options] file | mysql -uroot -proot_password database_name
    

    mysqlbinlog常用參數:

    –start-datetime 開始時間,格式 2020-06-19 18:00:00
    –stop-datetime 結束時間,格式同上
    –start-positon 開始位置,(需要查看binlog文件)
    –stop-position 結束位置,同上

    恢復備份數據和binlog數據前建議先登錄mysql后執行flush logs生成新的binlog日誌,這樣可以專註需要恢複數據的binlog文件。
    首先我們需要查看binlog日誌,在哪個位置進行了drop database操作:

    mysql> show binlog events in 'binlog.000003';
    +---------------+-----+----------------+-----------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------+
    | Log_name      | Pos | Event_type     | Server_id | End_log_pos | Info                                                                                                                                        |
    +---------------+-----+----------------+-----------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------+
    | binlog.000003 |   4 | Format_desc    |         1 |         125 | Server ver: 8.0.20, Binlog ver: 4                                                                                                           |
    | binlog.000003 | 125 | Previous_gtids |         1 |         156 |                                                                                                                                             |
    | binlog.000003 | 156 | Anonymous_Gtid |         1 |         235 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS'                                                                                                        |
    | binlog.000003 | 235 | Query          |         1 |         318 | BEGIN                                                                                                                                       |
    | binlog.000003 | 318 | Rows_query     |         1 |         479 | # INSERT INTO `product_category` SET `name` = '床上用品' , `create_time` = 1592707634 , `update_time` = 1592707634 , `lock_version` = 0      |
    | binlog.000003 | 479 | Table_map      |         1 |         559 | table_id: 139 (hotel_server.product_category)                                                                                               |
    | binlog.000003 | 559 | Write_rows     |         1 |         629 | table_id: 139 flags: STMT_END_F                                                                                                             |
    | binlog.000003 | 629 | Xid            |         1 |         660 | COMMIT /* xid=2021 */                                                                                                                       |
    | binlog.000004 | 660 | Anonymous_Gtid |         1 |         739 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS'                                                                                                        |
    | binlog.000004 | 739 | Query          |         1 |         822 | drop database hotel_server /* xid=26 */                                                                                                     |
    +---------------+-----+----------------+-----------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------+
    

    根據上面的日誌,我們可以看到,在End_log_pos = 822 的位置執行了drop database操作,那麼使用binlog恢復的範圍就在2020-06-19 20:00:00 – 660 的位置。為什麼是660?因為drop database的上一個事務的提交是660的位置,命令如下:

    mysqlbinlog --start-datetime=2020-06-19 20:00:00 --stop-position=660 /var/lib/mysql/binlog.000003 | mysql -uroot -proot_password datbase_name
    

    如果你的範圍包括了822的位置,那麼就會幫你執行drop database命令了。不信你試試?
    執行完上面的命令,你的數據就會恢復到drop database前啦!開不開心,激不激動!

    總結

    因為mysql定時備份是在生產環境上必須的任務。是很常用的。所以我就迫不及待的寫博客。當然也很感謝我同事的幫助。這篇文章已經寫了三天了,因為我也是在不斷地試錯,不斷的更新文章。避免把錯誤的知識點寫出來。如果幫到你了,關注我一波唄!謝謝。

    個人博客網址: https://colablog.cn/

    如果我的文章幫助到您,可以關注我的微信公眾號,第一時間分享文章給您

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    新北清潔公司,居家、辦公、裝潢細清專業服務

    ※推薦評價好的iphone維修中心

  • 快速打造屬於你的接口自動化測試框架

    快速打造屬於你的接口自動化測試框架

    1 接口測試

    接口測試是對系統或組件之間的接口進行測試,主要是校驗數據的交換,傳遞和控制管理過程,以及相互邏輯依賴關係。
    接口自動化相對於UI自動化來說,屬於更底層的測試,這樣帶來的好處就是測試收益更大,且維護成本相對來說較低,是我們進行自動化測試的首選

    2 框架選型

    目前接口自動化的框架比較多,比如jmeter,就可以集接口自動化和性能測試於一體,該工具編寫用例效率不高;還有我們常用的postman,結合newman也可以實現接口自動化;Python+unittest+requests+HTMLTestRunner 是目前比較主流的測試框架,對python有一定的編碼要求;
    本期我們選擇robotframework(文中後續統一簡稱為RF)這一個比較老牌的測試框架進行介紹,RF是一個完全基於 關鍵字 測試驅動的框架,它即能夠基於它的一定規則,導入你需要的測試庫(例如:其集成了selenium的測試庫,即可以理解為操作控件的測試底層庫),然後基於這些測試庫,你能應用TXT形式編寫自己的關鍵字(支持python和java語言,這些關鍵字即你的庫組成),之後,再編寫(測試用例由測試關鍵字組成)進行測試;他支持移動端、UI自動化和接口自動化的測試

    3 環境搭建

    • python的安裝:目前選取的python3以上的版本,RF的運行依賴python
    • robotframework:參考https://www.jianshu.com/p/9dcb4242b8f2
    • jenkins:用於調度RF的用例執行環境
    • gitlab:代碼倉庫

    4 需求

    4.1 需求內容
    接口內容:實現一個下單,並檢查訂單狀態是否正常的場景;該需求涉及到如下三個接口

    • 下單接口
    • 訂單結果查詢接口
    • 下單必須帶上認證標識,生成token的接口

    環境覆蓋:需要支持能在多套環境運行,比如測試和預發布環境
    系統集成:需要能夠集成在CICD中,實現版本更新后的自動檢測

    4.2 用例設計
    4.2.1 用例設計,根據業務場景設計測試用例,方便後續實現

    4.2.2 測試數據構造,預置不同環境的測試數據,供實現調用

    5 整體實現架構

    接口測試實現層:在RF,通過引用默認關鍵字 RequestsLibrary (實現http請求)和通過python自定義關鍵字來完成用例實現的需求;
    jenkins調度:在jenkins上配置一個job,設置好RF用例執行的服務器和發送給服務器相關的RF執行的指令,並且在jenkins中配置好測試報告模板,這樣用例便可以通過jenkins完成執行併發送測試結果給項目干係人;
    生成用例執行的API:上圖中藍色部分,就是為了將jenkins的job生成一個可訪問api接口,方便被測項目的CICD集成;
    集成到被測系統CICD流程:將上面步驟中封裝的API配置在被測應用的__gitlab-ci.yml__中,完成整個接口自動化的閉環

    6 RF用例實現

    6.1 引用的內置關鍵字

    • RequestsLibrary 構造http的請求,get|post等請求
    getRequests
    # get請求的入參
        [Arguments]    ${url_domain}    ${getbody}    ${geturl}    ${getToken}
        Create session    postmain    ${url_domain}
    # 定義header的內容
        ${head}    createdictionary    content-type=application/json    Authorization=${getToken}    MerchantId=${s_merchant_id}
    # get請求
        ${addr}    getRequest    postmain    ${geturl}    params=${getbody}    headers=${head}
    # 請求狀態碼斷言
        Should Be Equal As Strings    ${addr.status_code}    200
        ${response_get_data}    To Json    ${addr.content}
    # 返回http_get請求結果
        Set Test Variable    ${response_get_data}	 
        Delete All Sessions
    

    6.2 自定義關鍵字

    • getEnvDomain 用於從自定義的configs.ini文件獲取對應環境的微服務的請求域名
      configs.ini的內容
    # 獲取configs.ini的內容
    import configparser
    def getEnv(path,env):
        config = configparser.ConfigParser()
        config.read(path)
        passport = config[env]['passport']
        stock=config[env]['stock']
        finance=config[env]['finance']
        SUP = config[env]['SUP']
        publicApi = config[env]['publicApi']
        publicOrder = config[env]['publicOrder']
        data_dict={'passport':passport,'stock':stock,'finance':finance,'SUP':SUP,'publicApi':publicApi,'publicOrder':publicOrder}
        return data_dict
    
    • excelTodict 用戶將excel中的內容作為字典返回
    import xlrd
    
    '''
    通用獲取excel數據
    @:param path excel文件路徑
    @:param sheet_name excel文件裏面sheet的名稱 如:Sheet1
    @:env 環境,是IT還是PRE
    '''
    def getExcelDate(path, sheet_name,env):
        bk = xlrd.open_workbook(path)
        sh = bk.sheet_by_name(sheet_name)
        row_num = sh.nrows
        data_list = []
        for i in range(1, row_num):
            row_data = sh.row_values(i)
            data = {}
            for index, key in enumerate(sh.row_values(0)):
                data[key] = row_data[index]
            data_list.append(data)
        data_list1 = []
        for x in data_list:
            #print('這是'+str(x))
            if(x.get('env')==env):
                data_list1.append(x)
        return data_list1
    
    • getToken 提供接口下單的授權token
    *** Keywords ***
    # 根據傳入的clientid、secret生成對應的token
    getToken
        [Arguments]    ${client_id}    ${client_secret}    ${url_domain}
        Create session    postmain    ${url_domain}
        ${auth}    createdictionary    grant_type=client_credentials    client_id=${client_id}    client_secret=${client_secret}
        ${header}    createdictionary    content-type=application/x-www-form-urlencoded
        ${addr}    postRequest    postmain    /oauth/token    data=${auth}    headers=${header}
        Should Be Equal As Strings    ${addr.status_code}    200
        ${responsedata}    To Json    ${addr.content}
        ${access}    Get From Dictionary    ${responsedata}    access_token
        ${token}    set variable    bearer ${access}
        Set Test Variable    ${token}
        Delete All Sessions
    
    • getAllDate 獲取該用例下的所有數據
    getAllData
        [Arguments]    ${row_no}
        getEnvDomain
        getBalance    ${row_no}
        getStockNum    ${row_no}
        getSupProPrice    ${row_no}
        getProPrice    ${row_no}
        Set Test Variable    ${publicOrderUrl}
        Set Test Variable    ${FPbalance}
        Set Test Variable    ${Pbalance}
        Set Test Variable    ${Sbalance}
        Set Test Variable    ${Jbalance}
        Set Test Variable    ${Cardnum}
        Set Test Variable    ${sprice}
        Set Test Variable    ${price}
        Set Test Variable    ${j_merchant_id}
        Set Test Variable    ${s_merchant_id}
        Set Test Variable    ${stock_id}
        Set Test Variable    ${p_product_id}
        Set Test Variable    ${s_product_id}
    
    
    • 實現demo
    *** Settings ***
    Test Template
    Resource          引用所有資源.txt
    
    *** Test Cases ***
    *** Settings ***
    Test Template
    Resource          引用所有資源.txt
    
    *** Test Cases ***
    01 下單卡密直儲商品
        [Tags]    order
        LOG    ---------------------獲取下單前的數量、餘額------------------------------------------
        getAllData    0
        ${Cardnum1}    set variable    ${Cardnum}
        ${FPbalance1}    set variable    ${FPbalance}
        ${Pbalance1}    set variable    ${Pbalance}
        ${Sbalance1}    set variable    ${Sbalance}
        ${Jbalance1}    set variable    ${Jbalance}
        ${CustomerOrderNo1}    Evaluate    random.randint(1000000, 9999999)    random
        ${Time}    Get Time
        log    ------------------------下單操作-------------------------------------------------------
        getToken    100xxxx    295dab07a9xxxx9780be0eb95xxxx   ${casUrl}
        ${input_cs}    create dictionary    memberId=${j_merchant_id}    clientId=1xxx079    userId=string    shopType=string    customerOrderNo=${CustomerOrderNo1}
        ...    productId=${p_product_id}    buyNum=1    chargeAccount=otest888888    notifyUrl=string    chargeIp=string    chargePassword=string
        ...    chargeGameName=string    chargeGameRole=string    chargeGameRegion=string    chargeGameSrv=string    chargeType=string    remainingNumber=0
        ...    contactTel=string    contactQQ=string    customerPrice=0    poundage=0    batchNumber=    originalOrderId=string
        ...    shopName=string    appointSupProductId=0    stemFromSubOrderId=123456    externalBizId=456789
        postRequests    ${publicOrderUrl}    ${input_cs}    /api/Order    ${token}
        ${data}    get from dictionary    ${responsedata}    data
        ${orderid}    get from dictionary    ${data}    id
        sleep    6
        ${getdata}    create dictionary    Id=${orderid}    PageIndex=1    PageSize=1
        getRequests    ${publicOrderUrl}    ${getdata}    /api/Order/GetList    ${token}
        ${datalist}    get from dictionary    ${response_get_data}    data
        ${data}    get from dictionary    ${datalist}    list
        ${dict}    set variable    ${data}[0]
        ${orderOuterStatus}    get from dictionary    ${dict}    orderOuterStatus
        LOG    ---------------------獲取下單后的數量、餘額----------------------------------------------
        getAllData    0
        ${Cardnum2}    set variable    ${Cardnum}
        ${FPbalance2}    set variable    ${FPbalance}
        ${Pbalance2}    set variable    ${Pbalance}
        ${Sbalance2}    set variable    ${Sbalance}
        ${Jbalance2}    set variable    ${Jbalance}
        ${sprice}    set variable    ${sprice}
        ${price}    set variable    ${price}
        log    ------------------斷言-----------------------------------------------------------------
        ${Cardnum3}    Evaluate    ${Cardnum1}
        ${Jbalance3}    Evaluate    ${Jbalance1}
        ${Sbalance3}    Evaluate    ${Sbalance1}
        ${Pbalance3}    Evaluate    ${Pbalance1}
        should be true    ${orderOuterStatus}==90
        should be true    ${Cardnum3}==${Cardnum2}
        should be true    ${Jbalance3}==${Jbalance2}
        should be true    ${Sbalance3}==${Sbalance2}
        should be true    ${Pbalance3}==${Pbalance2}
    
    

    7 集成到CICD流程

    7.1 jenkins配置job
    通過jenkins的參數化構建,定義it和pre兩套環境

    jenkins發送RF執行的命令

    7.2 封裝的jenkins_job的執行接口地址
    通過python的flask框架,根據測試和pre兩套環境包一層jenkins的job執行接口

    __author__ = 'paul'
    
    # !/usr/bin/env python
    # -*- coding: utf-8 -*-
    from flask import Flask, abort, request, jsonify
    import jenkins
    
    server = jenkins.Jenkins('http://10.0.1.xxx:80', username='xxx', password='fuluxxxx')
    
    app = Flask(__name__)
    
    tasks = []
    
    # it的測試集合http請求接口
    @app.route('/test/it', methods=['get'])
    def robot_Test_It():
        server.build_job('CI_FuluOrder', {'environment': 'IT'})
        return jsonify({'result': 'success'})
    
    # pre的測試集合http請求接口
    @app.route('/test/pre', methods=['get'])
    def robot_Test_Pre():
        server.build_job('CI_FuluOrder', {'environment': 'PRE'})
        return jsonify({'result': 'success'})
    
    if __name__ == "__main__":
        # 將host設置為0.0.0.0,則外網用戶也可以訪問到這個服務
        app.run(host="0.0.0.0", port=80, debug=True)
    
    

    7.3 將上述flask封裝的接口打包成鏡像
    根據dockerfile生成鏡像

    FROM python:3.6
    WORKDIR /app
    EXPOSE 80
    COPY .	.
    RUN pip install -r requirements.txt 
    ENTRYPOINT ["python","robotTestApi.py"]
    
    

    7.4 將鏡像部署到kubernetes,對外提供服務
    供觸發測試執行的調用入口 ,這部分封裝的接口部署在本地的k8s集群下ordermiddle

    IT: http://ordermiddle.xxx.cn/test/it
    pre:http://ordermiddle.xxx.cn/test/pre

    7.5 被測項目的CICD集成接口自動化測試
    gitlab目前採取直接對CICD腳本加入測試步驟,在部署到容器30秒后(考慮到容器在K8S啟動時間)調用測試接口

    7.6 發送測試報告

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※回頭車貨運收費標準

  • Java 多線程基礎(十)interrupt()和線程終止方式

    Java 多線程基礎(十)interrupt()和線程終止方式

    一、interrupt() 介紹

    interrupt() 定義在 Thread 類中,作用是中斷本線程

    本線程中斷自己是被允許的;其它線程調用本線程的 interrupt() 方法時,會通過 checkAccess() 檢查權限。這有可能拋出 SecurityException 異常。
    如果本線程是處於阻塞狀態:調用線程的 wait() , wait(long) 或 wait(long, int) 會讓它進入等待(阻塞)狀態,或者調用線程的 join(),join(long),join(long, int),sleep(long),sleep(long, int) 也會讓它進入阻塞狀態。若線程在阻塞狀態時,調用了它的 interrupt() 方法,那麼它的“中斷狀態”會被清除並且會收到一個 InterruptedException 異常。例如,線程通過 wait() 進入阻塞狀態,此時通過 interrupt() 中斷該線程;調用 interrupt() 會立即將線程的中斷標記設為 true,但是由於線程處於阻塞狀態,所以該“中斷標記”會立即被清除為 “false”,同時,會產生一個 InterruptedException 的異常
    如果線程被阻塞在一個 Selector 選擇器中,那麼通過 interrupt() 中斷它時;線程的中斷標記會被設置為 true,並且它會立即從選擇操作中返回。
    如果不屬於前面所說的情況,那麼通過 interrupt() 中斷線程時,它的中斷標記會被設置為 true。
    中斷一個“已終止的線程”不會產生任何操作。

    二、線程終止方式

    Thread中的 stop() 和 suspend() 方法,由於固有的不安全性,已經建議不再使用!
    下面,我先分別討論線程在“阻塞狀態”和“運行狀態”的終止方式,然後再總結出一個通用的方式。

    (一)、終止處於“阻塞狀態”的線程.

    通常,我們通過“中斷”方式終止處於“阻塞狀態”的線程
    當線程由於被調用了 sleep(),,wait(),join() 等方法而進入阻塞狀態;若此時調用線程的 interrupt() 將線程的中斷標記設為 true。由於處於阻塞狀態,中斷標記會被清除,同時產生一個InterruptedException 異常。將 InterruptedException 放在適當的位置就能終止線程,形式如下:

    public void run() {
        try {
            while (true) {
                // 執行業務
            }
        } catch (InterruptedException ie) {  
            // 由於產生InterruptedException異常,退出while(true)循環,線程終止!
        }
    }

    說明:

    在while(true)中不斷的執行業務代碼,當線程處於阻塞狀態時,調用線程的 interrupt() 產生 InterruptedException 中斷。中斷的捕獲在 while(true) 之外,這樣就退出了 while(true) 循環!

    注意:

    對 InterruptedException 的捕獲務一般放在 while(true) 循環體的外面,這樣,在產生異常時就退出了 while(true) 循環。否則,InterruptedException 在 while(true) 循環體之內,就需要額外的添加退出處理。形式如下: 

    public void run() {
        while (true) {
            try {
                // 執行任務...
            } catch (InterruptedException ie) {  
                // InterruptedException在while(true)循環體內。
                // 當線程產生了InterruptedException異常時,while(true)仍能繼續運行!需要手動退出
                break;
            }
        }
    }

    說明:

    上面的 InterruptedException 異常的捕獲在 whle(true) 之內。當產生 InterruptedException 異常時,被 catch 處理之外,仍然在 while(true) 循環體內;要退出 while(true) 循環體,需要額外的執行退出while(true) 的操作。

    (二)、終止處於“運行狀態”的線程

    通常,我們通過“標記”方式終止處於“運行狀態”的線程。其中,包括“中斷標記”和“額外添加標記”。

    1、通過“中斷標記”終止線程

    public void run() {
        while (!isInterrupted()) {
            // 執行任務...
        }
    }

    說明:

    isInterrupted() 是判斷線程的中斷標記是不是為 true。當線程處於運行狀態,並且我們需要終止它時;可以調用線程的 interrupt() 方法,使用線程的中斷標記為 true,即 isInterrupted() 會返回true。此時,就會退出while循環。
    注意:interrupt() 並不會終止處於“運行狀態”的線程!它會將線程的中斷標記設為 true。

    2、通過“額外添加標記”終止線程

    private volatile boolean flag= true;
    protected void stopTask() {
        flag = false;
    }
    public void run() {
        while (flag) {
            // 執行任務...
        }
    }

    說明:

    線程中有一個 flag 標記,它的默認值是 true;並且我們提供 stopTask() 來設置 flag 標記。當我們需要終止該線程時,調用該線程的 stopTask() 方法就可以讓線程退出 while 循環。
    注意:將 flag 定義為 volatile 類型,是為了保證 flag 的可見性。即其它線程通過 stopTask() 修改了 flag 之後,本線程能看到修改后的 flag 的值。

    (三)、通過方式

    綜合線程處於“阻塞狀態”和“運行狀態”的終止方式,比較通用的終止線程的形式如下:

    public void run() {
        try {
            // 1. isInterrupted()保證,只要中斷標記為true就終止線程。
            while (!isInterrupted()) {
                // 執行任務...
            }
        } catch (InterruptedException ie) {  
            // 2. InterruptedException異常保證,當InterruptedException異常產生時,線程被終止。
        }
    }
    1、isInterrupted()保證,只要中斷標記為 true 就終止線程。
    2、InterruptedException 異常保證,當 InterruptedException 異常產生時,線程被終止。

    三、示例

    public class InterruptTest {
        public static void main(String[] args) {
            try {
                Thread t1 = new MyThread("t1"); // 新建線程t1
                System.out.println(t1.getName() + "[" + t1.getState() + "] is new.");
                
                t1.start();// 啟動線程t1
                System.out.println(t1.getName() + "[" + t1.getState() + "] is started.");
                
                Thread.sleep(300);// 休眠300毫秒,然後主線程給t1發“中斷”指令,查看t1狀態
                t1.interrupt();
                System.out.println(t1.getName() + "[" + t1.getState() + "] is interrupted.");
                
                Thread.sleep(300);// 休眠300毫秒,然後查看t1狀態
                System.out.println(t1.getName() + "[" + t1.getState() + "] is interrupted now.");
            }catch(InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    class MyThread extends Thread{
        public MyThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            try {
                int i = 0;
                while(!isInterrupted()) {
                    Thread.sleep(100);// 休眠100毫秒
                    ++i;
                    System.out.println(Thread.currentThread().getName() + "[" + this.getState() + "] loop " + i);
                }
            }catch(InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "[" + this.getState() + "] catch InterruptedException");
            }
        }
    }
    // 運行結果
    t1 [ NEW ] is new.
    t1 [ RUNNABLE ] is started.
    t1 [ RUNNABLE ] loop 1
    t1 [ RUNNABLE ] loop 2
    t1 [ RUNNABLE ] loop 3
    t1 [ RUNNABLE ] catch InterruptedException
    t1 [ TERMINATED ] is interrupted.
    t1 [ TERMINATED ] is interrupted now.

    說明:

    ①、主線程 main 中通過 new MyThread(“t1”) 創建線程 t1,之後通過 t1.start() 啟動線程 t1。
    ②、t1 啟動之後,會不斷的檢查它的中斷標記,如果中斷標記為“false”;則休眠 100ms。
    ③、t1 休眠之後,會切換到主線程main;主線程再次運行時,會執行t1.interrupt()中斷線程t1。t1收到中斷指令之後,會將t1的中斷標記設置“false”,而且會拋出 InterruptedException 異常。在 t1 的 run() 方法中,是在循環體 while 之外捕獲的異常;因此循環被終止。

    我們對上面的結果進行小小的修改,將run()方法中捕獲InterruptedException異常的代碼塊移到while循環體內。

    public class InterruptTest {
        public static void main(String[] args) {
            try {
                Thread t1 = new MyThread("t1"); // 新建線程t1
                System.out.println(t1.getName() + " [ " + t1.getState() + " ] is new.");
                
                t1.start();// 啟動線程t1
                System.out.println(t1.getName() + " [ " + t1.getState() + " ] is started.");
                
                Thread.sleep(300);// 休眠300毫秒,然後主線程給t1發“中斷”指令,查看t1狀態
                t1.interrupt();
                System.out.println(t1.getName() + " [ " + t1.getState() + " ] is interrupted.");
                
                Thread.sleep(300);// 休眠300毫秒,然後查看t1狀態
                System.out.println(t1.getName() + " [ " + t1.getState() + " ] is interrupted now.");
            }catch(InterruptedException e) {
                e.printStackTrace();
            }
            
        }
    }
    class MyThread extends Thread{
        public MyThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            int i = 0;
            while(!isInterrupted()) {
                try {
                    Thread.sleep(100); // 休眠100ms
                } catch (InterruptedException ie) {  
                    System.out.println(Thread.currentThread().getName() +" [ "+this.getState()+" ] catch InterruptedException.");  
                }
                i++;
                System.out.println(Thread.currentThread().getName()+" [ "+this.getState()+" ] loop " + i);  
            }
        }
    }
    // 運行結果
    t1 [ NEW ] is new.
    t1 [ RUNNABLE ] is started.
    t1 [ RUNNABLE ] loop 1
    t1 [ RUNNABLE ] loop 2
    t1 [ TIMED_WAITING ] is interrupted.
    t1 [ RUNNABLE ] catch InterruptedException.
    t1 [ RUNNABLE ] loop 3
    t1 [ RUNNABLE ] loop 4
    t1 [ RUNNABLE ] loop 5
    t1 [ RUNNABLE ] loop 6
    t1 [ RUNNABLE ] is interrupted now.
    t1 [ RUNNABLE ] loop 7
    ...... // 無限循環

    說明:

    程序進入了死循環了。

    這是因為,t1在“等待(阻塞)狀態”時,被 interrupt() 中斷;此時,會清除中斷標記(即 isInterrupted() 會返回 false),而且會拋出 InterruptedException 異常(該異常在while循環體內被捕獲)。因此,t1理所當然的會進入死循環了。
    解決該問題,需要我們在捕獲異常時,額外的進行退出 while 循環的處理。例如,在 MyThread 的 catch(InterruptedException) 中添加 break 或 return 就能解決該問題。

    下面是通過“額外添加標記”的方式終止“狀態狀態”的線程的示例:

    public class InterruptTest {
        public static void main(String[] args) {
            try {
                MyThread t1 = new MyThread("t1"); // 新建線程t1
                System.out.println(t1.getName() + " [ " + t1.getState() + " ] is new.");
                
                t1.start();// 啟動線程t1
                System.out.println(t1.getName() + " [ " + t1.getState() + " ] is started.");
                
                Thread.sleep(300);// 休眠300毫秒,然後主線程給t1發“中斷”指令,查看t1狀態
                t1.stopTask();
                System.out.println(t1.getName() + " [ " + t1.getState() + " ] is interrupted.");
                
                Thread.sleep(300);// 休眠300毫秒,然後查看t1狀態
                System.out.println(t1.getName() + " [ " + t1.getState() + " ] is interrupted now.");
            }catch(InterruptedException e) {
                e.printStackTrace();
            }
            
        }
    }
    class MyThread extends Thread{
        private volatile boolean flag = true;
        public void stopTask() {
            flag = false;
        }
        public MyThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            synchronized (this) {
                int i = 0;
                while(flag) {
                    try {
                        Thread.sleep(100); // 休眠100ms
                    } catch (InterruptedException ie) {  
                        System.out.println(Thread.currentThread().getName() +" [ "+this.getState()+" ] catch InterruptedException.");  
                        break;
                    }
                    i++;
                    System.out.println(Thread.currentThread().getName()+" [ "+this.getState()+" ] loop " + i);  
                }
            }
            
        }
    }
    // 運行結果
    t1 [ NEW ] is new.
    t1 [ RUNNABLE ] is started.
    t1 [ RUNNABLE ] loop 1
    t1 [ RUNNABLE ] loop 2
    t1 [ RUNNABLE ] loop 3
    t1 [ RUNNABLE ] is interrupted.
    t1 [ TERMINATED ] is interrupted now.

    四、interrupted() 和 isInterrupted()的區別

    interrupted() 和 isInterrupted()都能夠用於檢測對象的“中斷標記”。
    區別是,interrupted() 除了返回中斷標記之外,它還會清除中斷標記(即將中斷標記設為 false);而 isInterrupted() 僅僅返回中斷標記

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

  • JVM 總結

    JVM 總結

    JVM GC 總結。

    周志明大大的《深入理解Java虛擬機》出第三版了,早早的買了這本書,卻一直沒有花時間看。近來抽空溫習了一下,感覺又有了新的收穫。這裏簡單總結下。

    GC的由來

    由於的動態性,操作系統將堆交由給了開發者自己管理,手動申請,手動釋放。對於C++,則是將這個權限繼續交給了開發者,而對於Java,則是將這個過程自動化了。為什麼要釋放內存呢?最簡單的原因就是操作系統一共給你了4G的內存空間,你需要的時候,就去借用。有借有還,再借不難,只借不還,最後4G內存空間被用完了,你就無法再申請新的內存了。內存泄漏,就是只借不還。

    JVM在操作系統與開發者之間又封裝了一層,間接的接管了內存的劃分。同時也將堆統一管理起來,使得開發者只管借用內存,由JVM負責回收,了解JVM的回收機制,明白它的原理,能讓開發者在不同的場景下,定製不同的回收規則,提高回收效率。

    關於GC的思考

    如果讓我設計一個能自動回收垃圾的虛擬機,我會怎麼設計呢?

    • 什麼時候開始回收?
    • 怎麼判斷這部分內存可以回收?
    • 怎麼回收這部分的垃圾?

    這3個問題,也是JVM開發者一直在思考的問題。之前簡單了解過JVM,就知道JVM會有Stop The World的問題,這對於用戶體驗來說非常不好,其根本原因便是因為在回收垃圾的時候,用戶線程可能會修改這部分內存,如果不暫停用戶線程,則可能會導致嚴重的問題,而如何減少Stop The World的時候,甚至讓其消失,是各個垃圾回收器一直追求的目標。

    哪些內存可以回收?

    對於一個對象來說,當不存在任何一個引用能夠訪問到這個對象的時候,則說明這個對象可以進行回收。因為沒有任何引用指向這個對象,那麼這個對象就不能被讀或寫。

    • 引用計數法

      前面說判斷一個對象可以被回收的標準就是是否還有引用指向這個對象,所以最容易想到的便是引用計數法,通過判斷一個對象的引用數量即可,可是這樣無法判斷兩個循環引用的對象。

    • 可達性分析

      可達性分析指的是從目前程序中正在使用的所有引用的對象出發,循環遍歷所有能找到的對象。

      作為出發的點的這些對象,被稱為GC Roots

      GC Roots主要包括以下幾種:

      • 在虛擬機棧(比如棧幀中的本地遍歷表)中引用的對象
      • 靜態屬性引用的對象
      • 常量池引用對象(比如
        String Table
      • 本地方法棧引用的對象
      • Java虛擬機內部的引用對應的對象
      • 所有被同步鎖持有的對象

      總體來說,就是當前程序中正在被使用的引用所指向的對象會被作為GC Roots

    GC Roots出發,依次查找,就能標記出當前存活的對象。但是標記這個過程,細節上依然存在問題:

    • STW : 標記是通過引用查找對象的,如果在標記過程中,用戶修改了引用的對象,那麼會導致不可預估的後果,因此一般標記過程中,是會STW

    • 跨代標記 : 現在的垃圾回收器,大多數都是分代,或者分區域回收的,也就是說,可能進行垃圾回收的時候,不是標記所有的垃圾,而是標記一部分,比如老年代或者新生代。此時就存在一個問題,跨代引用。比如一個新生代的對象,僅僅被一個老年代對象引用的話,對於Yong GC來說,是不會掃描老年代對象的,這個時候就會造成誤判。解決這個誤判的方法便是記憶集(Remembered Set),記憶集通過AOP技術生成寫屏障來維護。

      前面說了從GC Roots開始掃面,那分代收集的,怎麼知道哪些對象是新生代的,哪些對象是老年代的呢?因為GC Roots是包含了所有引用的。後面想想,其實對象的分代信息是存放在對象頭裡面的。在掃描GC Roots的時候,只保留新生代的對象即可。這樣基本能保證掃描到的是新生代對象,然後老年代對新生代引用交給記憶集實現就行(自己的猜測,沒有證據)

      JVM書中說道通過AOP生成的寫屏障會使得只要有更新操作,無論更新的是不是老年代對新生代對象的引用,都會使卡表變髒,不過這樣的代價相對來說是能接受的。

    • GC Roots 需要掃描的引用過多 :隨着現在Java應用越做越大,Java堆也越來越大,GC Roots的掃描是需要STW的,如果每次GC都逐個掃描,會非常的浪費時間。解決這個問題的辦法就是OopMap,使用OopMap記錄應用程序所存放的引用,每次需要GC的時候掃描這個OopMap即可生成對應的GC RootsOopMap通過安全點和安全區域來維護,只有在安全點或安全區域的時候,才更新OopMap和進行垃圾回收。

    • 併發標記過程可能丟失存活的對象 :從CMSG1,都將從GC Roots出發標記存活對象的過程修改成併發的,這樣會需要解決的問題就是標記過程中如果用戶修改了對象的引用,可能會導致本應該存活的對象”丟失“(可以通過三色標記分析),相應的解決方案便是破壞存活對象消失的必要條件,分別是增量更新(Incremental Upate)和原始快照(Snapshot At The Begin,SATB),增量更新破壞的是第一個條件,每插入一個引用,就都記錄下來,而原始快照破壞的是第二個條件,每刪除一個,都將其記錄下來。

      增量更新和併發快照也是通過前面所說的AOP技術生成寫屏障來維護

    通過以上分析以及解決方案,基本明白了怎麼標記那些內存可以回收,接下來需要分析的就是什麼時候開始回收

    什麼時候開始內存回收?

    對於內存回收來說,開始也需要有一定的講究,理論上來說,隨時隨地都可以開始內存回收,但是如果回收時使用的內存過多,會導致GC時間過程,進而STW時間也會很長,如果回收過於頻繁,又會導致吞吐量下降,畢竟每次掃描GC Roots都回STW的。

    同時,前面還說過,對於用戶線程來說,需要將用戶線程運行到安全點,更新對應的OopMap,才能開始垃圾回收。

    因此,對應何時GC,有以下幾點分析:

    • 對於新生代來說,一般新生代滿了(Eden + Survivor1)就會開始進行(Yong/Minor GC

    • 對於老年代來說,一般是老年代滿了了會開始Full/Major GC

      注意:這裏的滿了,需要根據具體的回收器不同,來衡量真正的滿,對於沒有併發過程的GC,老年代滿一般指的是真正到達100%,已經無法分配內存了,對於有併發過程的GC,則需要預留出來空間給用戶線程在併發過程中同時申請內存,如果預留內存過小,則會使用非併發垃圾回收器進行Full GC

      CMS: -XX:CMSInitiatingOccupancyFraction 設置,默認92% (JDK 8),表示當老年代垃圾佔用到92%就開始老年代回收, JDK 9后便無法使用CMS

      G1: -XX:G1ReservePercent設置,默認為10,表示當整個Java堆使用到達90%,就開始回收。同時配合的參數還有-XX:InitiatingHeapOccupancyPercent=n,默認值為45,表示使用率到達45%就啟動標記周期。這裏的GCMixed GC

      一般來說,只有CMS才有Major GC,其他老年代GC都會回收整個Java堆,也稱為Full GC

    • 統計得到的Minor GC晉陞到老年代的平均大小大於老年代剩餘的空間。(JDK 6 之後已經刪除了擔保規則)

    • GC併發失敗(concurrent mode failure): 情況如前面說的,併發標記過程中,又出現了新生代晉陞的情況,但是此時老年代剩下的內存不足夠放下晉陞的對象的時候,會生導致Full GC

      這裏的Full GC和情況1中說到達預留空間的GC不一樣,情況1是正常進行的GC,而這個併發失敗卻是GC過程中出現了異常,一般需要切換到非併發GC,此時性能會大大下降

    • 方法區區域被使用完畢:JDK 8之後將方法區從Perm Gen替換成了元空間,一般來說元空間大小理論上等於本地內存大小,不過元空間有一個默認初始值,到達默認初始值后,會通過Full GC擴大

      注意:G1只有Yong GCMixed GC。沒有Full GC的概念,也就是說如果需要回收方法區的話,只能退化為Serial GC進行Full GC

      CMS可以通過-XX:+CMSClassUnloadingEnabled設置併發回收方法區

    • 最大連續空間裝不下大對象:對於CMS,基於標記-清除算法來說,即使空間足夠,但是由於內存碎片,裝不下分配的大對象時,會進行一次Full GC,對於G1來說,當分配巨型對象的時候,如果在老年代無法找到連續的Humongous的時候,會進行Full GC

    • 用戶執行System.gc(),可以通過-XX:+DisableExplicitGC屏蔽

    怎麼回收這些內存

    最後一步便是怎麼回收這些內存。怎麼回收,書中介紹不多,總體來說有以下三種:

    • 標記-清除(
      Mark-Sweep):最原始的方法,實現簡單,不用移動對象,很容易做到不用
      Stop The Word,但是缺點也很致命,容易產生內存碎片。標記清除的速度一般,
      Mark階段與活對象的數量成正比,
      Sweep階段與整堆大小成正比。目前只有
      CMS使用這種回收方案
    • 標記-複製(
      Mark-Copying):基於標記-清除修改的垃圾回收算法,需要移動對象。 前期標記,然後複製活下來的對象到另一個區域,再總體回收整塊區域。標記複製算法對於新生代這種專門放朝生夕死的對象效率非常高,因為存活下來的對象少,所以
      Mark階段和
      Copying階段花費的時間都會比較少,幾乎所有的分代
      GC新生代都是使用的這種算法
    • 標記-整理(
      Mark-Compact): 基於標記-清除算法修改的垃圾回收器,需要移動對象。前期標記,然後將所有對象移動到一起,再對剩餘的區域進行回收,速度最慢,但是不會產生內存碎片。

    對於新生代使用標記-複製算法,是毋庸置疑的。但是對於老年代,使用標記清除還是標記整理,需要有一定的考量。因為使用標記-清除,不用移動對象,速度會相對來說比較快,但是由於存在內存碎片,無法使用指針碰撞的方式分配內存,而不得不使用“分區空閑分配鏈表”來解決內存分配的問題,這樣會對在內存分配帶來一定的效率影響,而標記-整理算法需要移動對象,特別是對於老年代這種大對象來說,移動這些對象將是一種極為負重的操作,但是標記-整理不會產生內存碎片。

    因此,基於以上考慮,對於CMS這種側重響應速度,致力於減少STW時間的回收器來說,選擇了標記-清除算法,但是由於內存分配是一個非常頻繁的操作,使用”分區空閑分配鏈表”會降低整個垃圾回收器的吞吐量,因此,對於Parllel Scavenge這種注重回收吞吐的垃圾回收器來說,選擇了標記-整理算法。當然,對於G1則是吞吐和響應速度都比較注重,權衡之下,選擇了標記-整理(全局)算法。

    GC的概念,到這裏基本總結完畢,但是,如果僅僅是理論,只是讓我們記着一些概念性的東西,接下來,我會結合CMSG1GC日誌以及《深入理解JVM》第四章的內容,聊一聊如何分析以及查看GC過程,簡單介紹如果進行GC調優。

    個人公眾號:

    不定期更新一些經典Java書籍總結。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • 聯合國示警:氣候變遷加劇阿拉伯地區衝突局勢

    環境資訊中心綜合外電;姜唯 編譯;林大利 審校

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?

  • 美國迪士尼致力環保 將停用一次用塑膠吸管

    摘錄自2018年7月27日蘋果日報美國報導

    美國娛樂巨頭華特迪士尼公司( Walt Disney Company)昨天(26日)宣示,明年中期以前,迪士尼樂園等將停止使用一次性塑膠吸管。鑑於塑料垃圾造成的海洋污染日益嚴重,為保護地球環境,歐美正在推廣相同的措施。

    迪士尼指出,此舉將可每年減少1.75億根以上的吸管、1.3億根攪拌棒,強調本次嘗試是迪士尼履行環保責任的一環。

    共同社則報導,據與迪士尼方面簽訂許可協議的東方樂園公司稱,由於位於千葉縣浦安市的東京迪士尼度假區,運營母體不同,因此不受此次華特迪士尼公司決定的影響。然而該公司也指,「正在研究減少塑料廢棄物」。

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    新北清潔公司,居家、辦公、裝潢細清專業服務

    ※推薦評價好的iphone維修中心

  • 葡西法三國簽署能源互聯協議 法國將關閉所有煤電廠

    摘錄自2018年7月28日新華社報導

    葡萄牙、西班牙和法國27日在葡萄牙首都里斯本舉行的第二屆能源互聯峰會上正式簽署了三國能源互聯協議。

    根據協議,西葡兩國同歐洲的能源互聯水平到2020年達到10%,2030年達到15%。此外,歐盟委員會將投資5.7億歐元在西班牙以北的比斯開灣建造一個用於連接西班牙、葡萄牙和法國的電力互聯項目。

    葡萄牙總理科斯塔、西班牙首相桑切斯和法國總統馬克宏在會後舉行了聯合記者會。馬克宏表示,最晚到2022年,法國將關閉所有煤電廠。

    科斯塔說,葡萄牙計劃到2020年使清潔能源占比超過60%,葡萄牙在逐步減少煤電行業投入的同時尋求清潔能源出口。

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※回頭車貨運收費標準